liildev

Notes on splitting responsibilities between a TanStack Start frontend and a NestJS API — what to colocate, what to separate, and where the boundaries actually live.

architecturetanstacknestjsfullstack

TanStack Start is still young, but I've shipped two production apps with it — ContentumAI and Tantana. Both pair it with NestJS on the backend. Here's what I've learned about where to draw the lines.

Why this stack

TanStack Router's type-safe routing and file-based structure give you the frontend DX you expect from Next.js, without the server-component model that creates friction when you have a real API layer. Start adds SSR and server functions on top, but you can opt out of both where they don't make sense.

NestJS on the backend gives you a structured module system, DI, guards, interceptors, and a clear separation between domain logic and HTTP concerns. For anything beyond a toy project, that structure pays off.

The boundary problem

The hardest architectural question in this setup is: what goes in TanStack Start's server functions, and what goes in NestJS?

My answer: almost nothing belongs in server functions beyond session reads and redirects. Server functions are great for pulling auth context from cookies and forwarding to the API. They're not the right place for business logic, database access, or complex validation.

Why? Because anything you put in server functions lives in the frontend deployment, not your API layer. That means it doesn't benefit from NestJS guards, interceptors, or the module system. It's harder to test in isolation. And if you ever need to expose the same logic to a mobile app or another client, you'll have to extract it anyway.

What the architecture actually looks like

TanStack Start (Vercel / Cloudflare):
  - File-based routing
  - Server functions: auth checks, session reads, API proxying
  - Client: React Query for all data fetching
  - No business logic

NestJS (Render / Railway):
  - All domain logic
  - Auth (JWT + refresh tokens)
  - All database access via Drizzle
  - Stripe/Paddle webhooks
  - Background jobs

React Query on the client calls the NestJS API directly (not through server functions). The session token lives in a cookie and gets read by NestJS guards. TanStack Start server functions handle the login flow and set the cookie.

Type sharing

Generate types from the API (Swagger → types, or tRPC if you're on that path) and import them in the frontend. Don't duplicate. A shared types package in a monorepo makes this clean. A generated api.d.ts file from OpenAPI works too.

What I'd do differently

I'd invest in tRPC earlier. The type safety from shared procedures is better than generated Swagger types for complex input/output shapes. The tradeoff is a tighter coupling between frontend and backend deployments — worth it for small teams.