I recently built my personal blog using Rails as a headless CMS and Next.js for the frontend. The setup is straightforward: Rails handles content through a JSON API, and Next.js generates static pages from that data. I chose this architecture partly to learn how these systems work together, but also because it keeps things cleanly separated. Rails worries about content and data; Next.js handles the rendering and performance side of things.
What Worked Well
The big win was static site generation. Next.js pre-renders all the blog pages at build time, so the HTML is already there when you visit a post. It's noticeably faster than waiting for API calls or client-side rendering, making a huge difference in how snappy things feel for a blog. I'd rather have fast page loads than real-time updates for content that changes maybe once a week.
Using TypeScript for the frontend helped more than I expected. I defined interfaces for what the API should return, and it caught a bunch of places where the Rails JSON didn't quite match what Next.js was expecting. While not perfect, it prevents a lot of stupid mistakes.
I also appreciated being forced to think API-first. Instead of having Rails render HTML directly, everything goes through JSON endpoints. This made me more deliberate about what data the backend should provide and how it's structured. This fosters good habits for working on any separated frontend/backend project.
The Tricky Parts
Static generation is great until you realize your content is stale. I'd add a new post to Rails, check the site, and find nothing. It took me a minute to remember I needed to rebuild. Eventually, I set up Incremental Static Regeneration (ISR) so pages refresh on demand, but understanding why the content wasn't updating was the first step.
Keeping the API contract in sync between Rails and Next.js was harder than it should have been. I'd change a field name in Rails, forget to update the TypeScript interface, and boom, broken frontend. It taught me to treat the API like an actual contract that both sides need to respect.
Next.js routing with generateStaticParams worked fine once I got it, but the learning curve was steeper than I expected. The slug format in Rails had to match exactly what Next.js expected in the URL. When they didn't match, I'd get confusing 404 errors that took longer to debug than they should have.
What I Learned
- API design ended up mattering more than I thought it would. Consistent field names, proper error codes, and clear data shapes make the frontend way easier to work with.
- Static generation is powerful, but you need a plan for how content updates. ISR worked for me, but webhooks or scheduled rebuilds could work too. Thinking about your update frequency upfront is crucial.
- TypeScript reduces a lot of friction once you commit to it. Combined with a well-designed backend API, it cuts down the feedback loop for catching errors before they hit production.
- Decoupled systems force you to think about boundaries more carefully. The separation means Rails doesn't need to know about frontend rendering, and Next.js doesn't care about the database structure, making both sides easier to reason about.
Next Steps
I'm working on a few improvements:
- On-demand revalidation: Automatically update pages when new content is published, without needing a full rebuild.
- Search functionality: Add filtering and search to the blog index. I'll probably go with a client-side solution to keep things simple.
- SEO improvements: Better metadata, structured data, and Open Graph tags for discoverability.
This project reinforced that even simple applications involve real design decisions around performance, data freshness, and system boundaries.