I’ve worked with React since the Create React App days—then moved on to Next.js, Remix, and later Vike.
Along the way, I picked up a lot of best practices to solve real-world company problems while still keeping a solid developer experience—things like SSR, CSR, hooks, state management, and atomic component design.
Then about a year ago, I got a project with very different constraints:
full server-side only
strictly using Express.js
single port setup
At first, I tried to keep React in the stack by using it just for the frontend instead of a traditional templating engine.
But then the requirement changed: “zero build” project.
Just:
npm start
…and it should run in production.
At that point, React was no longer viable. Even without TypeScript, it still requires a build step. Using CDN wasn’t an option either.
After a lot of research, experimenting, and even brainstorming with AI—trying different libraries and approaches—I ended up going with EJS + Alpine.js.
Fully server-driven.
No client-side rendering, no Redux or heavy state management, no complex routing layer on the frontend. And honestly, the best part: no CORS headaches and no hydration issues.
What surprised me is that most of what I used to do with React… I can still achieve with Alpine. Component modularity is handled cleanly through EJS includes.
And it got me thinking:
Are we sometimes just creating complexity for problems that were already solved?
Top comments (0)