Web development continues to move fast, especially on the front-end, and 2025 was no exception. Faster bundlers, new frameworks, and the undoubtedly march towards TypeScript and React homogeneity has brought us reusable components, better experiences (with partial UI changes) and long build times with split (front-end and back-end) codebases glued together by copious amounts of GraphQL – and that’s fine, if you have the specialized teams to handle it.
But what if you don’t? Instead you have a small team of brilliant generalists looking for the biggest productivity boost possible? Well, we ported www.scanii.com from a conventional webpack+typescript spring boot application to the stack outlined below with these nice gains:
- Consolidated codebase, with a simple “Run” target in IntelliJ
- Frontend build times down to 0 with automatic reloading
- Partial page renderings (like a SPA) and you can still use React if you choose to
- 100 points on lighthouse performance score
- Easier debugging without source maps or a transpilation
Here’s how we did it:
Moving all rendering Server Side
1. Pick a template engine
Start by picking a template rendering engine supported by Spring Boot. We went with JTE mostly for personal preference, it feels more productive since the entire JDK is available in the template and the view model maps nicely to sending Page objects down – I could write a whole blog post about this alone. In any case, any template engine of your choice should work
2. Ditch the bundler
For this, you need to move to using importmaps although it might seem exotic, it is the same direction that Rails is pursuing and part of the HTML standard. In essence, it allows you to map JavaScript module import paths to other JavaScript files. Here’s ours:

Something else that turned out to be super cool is that we could tie in importmaps with webjars and have those dependencies served directly from our spring boot app without needing a third party CDN. The other interesting thing in that screenshot is our main entry point “/js/index.js” and as you would expect that’s where we link all of our javascript so the browser can load them:

3. Deliver interactivity
This is the idea that for certain actions the browser full page reload cycle is just too long and makes your application feel sluggish. It is the reason SPA’s were originally created for, shipping most of the app to the client at once and perform smaller partial server updates. To achieve SPA levels of interactivity with a single codebase we again followed Rails lead and decided for Turbo and Stimulus from Hotwire.dev. This gave us automatic partial page updates (with Turbo) and a familiar MVC framework (with Stimulus) for the Javascript we still needed. With that said, with importmaps in place, we could have stuck with React for component rendering and that is something we could still introduce in the future for certain components that it just doesn’t make sense to port to Stimulus.
Pain Points
Like everything else in tech, it’s a game of tradeoffs. Here are some of the pain points we ran into:
Missing HOT Reloading
One nice thing about modern front-end stacks is the ability to see your changes immediately (well after the bundler does its thing) reflected in the UX. We were able to close this gap with some minimal logic client side and server-side. On the client side we poll for server side changes (and on the server we monitor the filesystem for changes. This turned out to be much faster than the front-end solution we had before.
Client side:
and the server side:
CSRF Protection
The moment you start porting large chunks of your codebase to server side rendering you will inevitable grow to hate current state of CSRF protection. Spring Security built-in tooling for protecting against those attacks relies on random tokens being included as a parameter in every form. Albeit there are ways to automate the process of adding a new input field to every form, it can be tedious and, more importantly, run into problems with Turbo page caching logic. To work around that pain point, we built a new CSRF filter based upon some nascent tech that relies on the browser on security header Sec-Fetch-Site – no more tokens sprinkled into templates or issues with partial pages reloads!
Here’s the core of our implementation for reference:
I plan on gauging the appetite of the Spring Security folks to have CSRF protection via Sec-Fetch-Site added in the future.
That’s all folks
All in all, I’m quite happy with the end result with both the local dev loop and the production performance improved by orders of magnitude. Now, if you like to see this all live, just hop over to www.scanii.com and view-source. It’s all there, no bundles, no transpilation, no source maps and, more importantly, no magic – ok, fine, very little magic.