Lessons from the Trenches: Building a Production-Grade App with Next.js and Node.js Microservices

Lessons from the Trenches: Building a Production-Grade App with Next.js and Node.js Microservices

A few months ago, I worked on a project that involved building a full-stack, production-grade application using:

  • Next.js (App Router) on the frontend

  • Node.js (Express) microservices on the backend

Modern stack. Familiar tools. We were confident going in.

But like many real-world systems, things that sounded simple on paper ended up demanding a deeper level of design, debugging, and coordination. This post isn’t a tutorial — it’s a reflection of the real-world challenges, what we learned, and how we solved them.


🔗 Frontend–Backend Integration: Looks Easy, Until It Isn't

One of the earliest hurdles was getting seamless communication between frontend and backend. We faced issues like:

  • CORS misconfigurations in dev and staging

  • Inconsistent API endpoints across environments

  • Proxy behaviors that differed between local and cloud

We started using shared TypeScript types across the frontend and backend via a monorepo, along with Zod for runtime validation. This ensured that:

  • API inputs/outputs stayed in sync

  • Type mismatches were caught early

  • Changes didn’t accidentally break contracts

It was extra effort, but paid off by reducing bugs from "invisible" backend changes.


🌐 HTTPS and mTLS: Security Beyond the Basics

Enforcing HTTPS locally with self-signed certificates turned out to be more annoying than expected — especially when browsers threw or blocked requests due to security policies.

But things got more serious when we introduced mTLS (mutual TLS) between microservices.

Why mTLS?

  • We wanted bidirectional authentication between services

  • Each service had to prove its identity, not just the client

What it involved:

  • Setting up a lightweight PKI system

  • Distributing trusted CA certs across environments

  • Automating certificate rotation and renewal using scripts

This added security and resilience, but brought complexity that required dedicated setup and ops support.


🛑 Resilience: Designing for Partial Failures

Some backend services weren’t always available — and the frontend was initially unprepared.

Our early versions would crash or show blank screens when a service went down. That’s when we realized: frontend resilience matters just as much as backend availability.

We addressed it by:

  • Wrapping critical API calls with retry logic (using and backoff strategies)

  • Adding fallback UI states for missing data

  • Introducing “read-only” modes for degraded functionality

This allowed the app to degrade gracefully, not catastrophically.


🐢 Performance: How SSR Can Hurt If You’re Not Careful

Next.js SSR (server-side rendering) is powerful, but it’s easy to misuse. Our performance issues came from:

  • Overfetching data in

  • Heavy synchronous processing during rendering

  • Bundling large utility libraries unnecessarily

Fixes that helped:

  • Dynamic imports for expensive or rarely-used components

  • Refactoring SSR to fetch only what’s needed for the view

  • Auditing and optimizing stateful components to avoid unnecessary rerenders

Our Core Web Vitals (especially TTFB and FCP) improved significantly after these changes.


🔁 State Management: Clean Data Flow Matters

Our state management strategy was initially all over the place — context everywhere, some Redux legacy, prop drilling, and bugs caused by hydration mismatches in SSR.

Eventually, we standardized on:

  • Zustand for local/global state (lightweight, no boilerplate)

  • React Query for API data (automatic caching, retries, and pagination)

This gave us:

  • Cleaner data flows

  • Separation of concerns between server state and client state

  • Far fewer bugs due to hydration issues

Bonus: React Query made it easy to implement background refreshes and stale cache invalidation.


📦 Shared Code: A Double-Edged Sword

We used a monorepo to share:

  • API types

  • Validation logic

  • Utility functions

It worked… until it didn’t.

We ran into:

  • Circular dependencies

  • Node-only imports leaking into frontend bundles

  • Weird build issues caused by mixing ESM and CommonJS

Eventually, we modularized shared logic properly and adopted TypeScript Project References. This kept boundaries clean and builds fast.

Lesson: shared code needs clear ownership and structure, or it becomes technical debt quickly.


🔐 Tokens, SSR & Middleware: Authentication Nightmares

Handling tokens is already tricky — throw in SSR and it gets worse.

We faced:

  • Expired tokens during SSR that returned stale content

  • Header leaks in server-rendered responses

  • Inconsistent cookie parsing across frontend middleware and backend APIs

Fixes:

  • Built a unified auth middleware (with a shared token handler)

  • Used httpOnly cookies with refresh logic

  • Implemented silent token renewal on the client side

This brought us close to a seamless auth experience without compromising on security.


📉 Observability: You Can’t Debug What You Can’t See

SSR issues in production were hard to debug — we couldn’t always replicate them locally.

What saved us:

  • Sentry for frontend + backend error tracking

  • Structured logging (JSON logs with correlation IDs across services)

  • Frontend performance monitoring (tracking slow routes and errors in real time)

With this setup, we could trace bugs across the stack — from browser event to backend log.


🚀 Deployments: Coordination Is Half the Battle

Frontend was deployed on Vercel, backend microservices on AWS ECS, with routing managed by Cloudflare.

Coordination challenges included:

  • Frontend being live before backend deploy completed

  • SSL cert mismatches across domains

  • Rollbacks on one side breaking the other

What helped:

  • Using feature flags to decouple deploys from releases

  • Adding health checks for backend APIs

  • Automating version sync during CI/CD pipelines


🧠 Key Takeaway: Fullstack ≠ Just Code on Both Ends

What I learned is that being “fullstack” doesn’t mean just writing both backend and frontend code.

It means thinking about:

  • Contracts and validation

  • Security and observability

  • Performance tuning

  • Resilience and degradation

  • Deployment strategy and infrastructure

This project reminded me that real engineering happens in the gray areas between layers — where design, infrastructure, and code converge.


If you’re working on or have shipped systems like this — I’d love to hear how you handled similar challenges.

#Nextjs #Nodejs #TypeScript #ExpressJS #React #Zustand #ReactQuery #SSR #CSR #DevOps #mTLS #FullstackEngineering #Microservices #DeveloperExperience #WebDev #RealWorldEngineering

To view or add a comment, sign in

Others also viewed

Explore topics