Why I Chose a Node.js Monorepo Over Next.js for a TypeScript SaaS Boilerplate
A practical framework for deciding when a Next.js starter is enough, and when a contract-first Node.js monorepo becomes the better long-term investment.
TL;DR
- Shape A (rendering-forward): Next.js, colocated handlers, one deployable. The right default when your primary risk is UI velocity and time to first paying user.
- Shape B (contract-forward): Split Node.js backend, explicit API contract, domain logic outside the routing layer. The right shape when billing, RBAC, and async jobs are first-class product features — not bolt-ons.
- Most TypeScript SaaS boilerplate starters optimize for Shape A. Refract optimizes for Shape B.
- Refract has already paid the Shape B setup tax. You do not have to choose between launch speed and correct architecture.
The TypeScript SaaS boilerplate market converges on the same shape. Next.js for the frontend. A thin backend if any. Stripe Checkout in the README. Ship fast, figure out the architecture later.
That convergence is rational. Next.js is a genuinely good framework. Colocated data fetching is fast to build. Vercel's deployment story is frictionless. If you have sixty days of runway and zero paying users, the fastest starter you can find is probably the right call.
This article is not about those sixty days.
It is about month six. When the first real pricing experiment touches billing state, seat limits, and access control in the same deploy. When a Stripe webhook arrives twice during a migration and two code paths both think they own the subscription transition. When a junior engineer adds a database query to a Server Action because that was the closest place to put it. When your TypeScript types cover the presentation layer but say nothing about whether the domain invariants held.
Most TypeScript SaaS starters are not designed for that moment. They are designed for the demo.
Refract is the TypeScript SaaS boilerplate at — a GraphQL-first Node.js monorepo built for Shape B from day one. This post explains what that means, who it is for, and why the setup cost you expect to pay is already paid.
The scars this post comes from
I spent five years at Loom and finished as tech lead of Billing during the Atlassian acquisition. The full story is in . The relevant part for this post: I ran pricing experiments on top of billing logic that had never been given a single owner.
Seat limits lived in one branch. Webhook handlers patched gaps that should have been domain rules. A "small" packaging change touched portal UI, a cron reconciler, and a Stripe dashboard configuration in the same week. When something broke at scale, billing was where the pager hurt.
That is not a failure of engineering discipline. It is what happens when a product's commercial logic is hosted inside a framework optimized for page rendering. The framework made it easy to put logic close to the UI. The logic drifted there. Over time, every pricing decision became a multi-layer surgery.
Refract is the boundary model I wish had existed. The describes what you pay when those edges stay fuzzy. This post describes the architectural choice that prevents it.
Two shapes of TypeScript SaaS
Before the Node.js versus Next.js comparison, you need one vocabulary decision. There are two fundamentally different shapes for a TypeScript SaaS product. Most framework debates are actually debates about which shape you are building.
Shape A — rendering-forward
The server is primarily a rendering surface. Data lives close to routes. The framework optimizes for how fast pages arrive and how little boilerplate stands between a database query and a component. Server Components, colocated loaders, and one deployable fit here. TypeScript safety flows within the framework's conventions: from query to component, within the boundaries the framework establishes.
Next.js is the best Shape A framework available. This is not a criticism. It is a description.
Shape B — contract-forward
The server is a domain authority. It owns business rules, enforces entitlements, and exposes a typed contract that any consumption surface — web portal, background workers, future mobile client, partner API — must satisfy. The backend is not a rendering layer with some API routes attached. It is a first-class deployable that happens to serve a frontend.
TypeScript safety in Shape B is structural rather than conventional. The schema defines the contract. Codegen propagates it to every consumer. The compiler enforces it whether or not anyone remembers to.
The failure mode of building Shape B on Shape A tooling
Most SaaS products start as Shape A and discover they needed Shape B around month six. The discovery happens when the first pricing experiment requires coordinating billing state, access control, and background jobs in the same deploy cycle.
At that point, Shape A tooling does not break. It bends. Business logic migrates toward the presentation layer because the framework makes that path easy. Route handlers accumulate domain knowledge. Server Actions start encoding billing rules. Webhook handlers grow into mini-services with their own notion of what state should be. TypeScript still compiles. The architecture has quietly become unmaintainable.
The refactor never happens. The product is live, customers are paying, and every sprint has higher priorities than untangling the foundation. You build on top of it instead.
Why not Next.js for Shape B — specifically
Next.js can do Shape B. Sophisticated teams build contract-forward products on it. This section is not a claim that it cannot. It is a claim about what it costs.
The colocated handler problem
Next.js App Router's natural pattern puts data fetching, mutation logic, and rendering in adjacent files or the same file. That locality is the feature. It is also the vector through which domain logic leaks into the presentation layer over time.
A senior engineer with strong opinions can prevent this. A team of four under sprint pressure usually cannot. An AI coding agent working autonomously almost certainly will not — it will put the database query where the framework's conventions suggest, which is next to the component.
Shape B requires domain logic to live outside the routing layer by default, not by discipline. Next.js can enforce this through strong conventions. It does not enforce it structurally.
Background jobs are a structural afterthought
Next.js is built around the request/response cycle. Background workers do not fit that model cleanly. The solutions — separate serverless functions, standalone worker processes, third-party queue services — all require building the boundary that Shape B needs, from scratch, outside the framework.
In a Node.js monorepo built for Shape B, background workers are first-class typed processes. They share domain utilities with GraphQL resolvers because those utilities live in a shared layer both surfaces import from. Adding a new background job means writing the job. The architecture for how it connects to domain logic already exists.
Webhook ownership has no structural home
Stripe delivers webhooks out of order. The same event can arrive twice. If checkout handlers and webhook consumers both mutate billing state, you get duplicate side effects and race conditions. The correct architecture assigns one canonical path per business action with .
Next.js gives you a route handler. What you do with it is your problem. The framework does not know about billing ownership. In a Shape B monorepo, the webhook trace is explicit:
invoice.paid
→ stripeWebhookConsumer (queue consumer)
→ invoicePaymentPaid (apps/backend/src/tools/queue/consumers/stripeWebhookConsumer/)
→ apps/backend/src/utilities/ (domain logic, e.g. billing.ts)
→ apps/backend/src/tools/paymentProcessor (typed adapter)
Each business action has one owner path with idempotency guards. Import boundaries keep Stripe out of resolvers and portal code. The compiler enforces the layers; conventions alone do not.
Why not Hono, Fastify, or NestJS instead of Express
A reasonable pushback: if the argument is "split backend for Shape B," why Express specifically? Hono and Fastify are faster. NestJS has stronger opinions about structure. Why not one of those?
On Hono and Fastify
Both are genuinely good frameworks with better TypeScript ergonomics out of the box than Express and better benchmark numbers. For a greenfield project with a single experienced engineer, either is a defensible choice.
Refract is infrastructure that someone else will run in production, extend under time pressure, and debug at 2am. The right framework for that context is the one with the deepest paper trail. Express has fourteen years of production usage, exhaustive failure mode documentation, and a middleware ecosystem that covers every edge case a SaaS product will encounter. When something unexpected happens, the answer exists. That is worth more than benchmark numbers.
Express also has no opinions about application structure. Refract's structure comes from the domain architecture — onion layers, functional core, tool adapters — not from framework conventions. A framework with strong structural opinions fights that. Express stays out of the way.
On NestJS
NestJS is the most credible alternative for Shape B. It has strong opinions about module boundaries, dependency injection, and separation of concerns. For a large team building a complex backend, those opinions are valuable.
For a SaaS boilerplate targeting technical founders and small teams, NestJS has two costs worth naming. The learning curve is real — the framework's conventions take time to internalize, and a founder who just wants to ship billing does not want to learn decorators and module graphs first. And NestJS's opinions can conflict with product-specific architecture decisions in ways that require working against the framework rather than with it.
Express gives you the structure you design. Refract gives you that structure pre-designed and pre-tested. The combination means you start with a Shape B architecture without learning a new framework to get it.
The GraphQL layer: why not tRPC or REST
Shape B requires an explicit API contract between the backend and every consumption surface. The contract question is separate from the framework question.
tRPC is excellent for a single TypeScript client. It collapses the client/server boundary and makes full-stack type safety feel like magic. The cost is that the contract is implicit — it exists in TypeScript, not in a schema a non-TypeScript client can consume. When the second client appears (mobile app, partner integration, internal tooling), tRPC requires duplication or rewriting.
REST with manual TypeScript types works. It requires discipline to keep in sync. It has no native codegen story for the frontend layer.
GraphQL with Apollo Server gives you a schema as an explicit contract, codegen that propagates it to every consumer, and field-level control over what each query returns. When a field changes on the backend, every downstream consumer fails at compile time. The schema is the source of truth. Everything else derives from it.
For Shape B specifically — where multiple clients consume the same backend, and where billing entitlements need to be enforced at the field level rather than the route level — GraphQL is the right contract layer. The doc maps schema to codegen to typed hooks.
The setup tax — and who has paid it
The honest reason most founders choose Next.js starters is not that they prefer colocated handlers. It is that setting up a correct Shape B monorepo takes time.
Not impossible time. A senior engineer who knows what they are doing can set up a clean GraphQL monorepo with typed adapter boundaries, background workers wired to shared domain logic, a codegen pipeline from backend schema to frontend hooks, and a local dev environment that mirrors production — in one to three weeks. Most founders do not have that week before they need to start building product.
Refract is that week, already spent.
What ships ready: Express + Apollo Server backend, React + Vite portal (one SPA, two route zones), Astro marketing site, BullMQ and SQS both implemented today (switch with one config value), Stripe billing with real subscription and webhook infrastructure, end-to-end GraphQL codegen in one Make target, full local stack in one command including Stripe CLI and LocalStack, (make deploy), Cursor rules preloaded for agent-safe development.
The has the full inventory with file paths. This post is the argument for why the shape exists. That post is the receipts.
When Shape A is still the right answer
Shape B has real costs. A Node.js monorepo has more moving parts than a Next.js app. A new contributor reads more files before they understand the system. make start boots eight processes where a simpler setup boots one.
Those costs are worth paying when your product has billing complexity, multiple client surfaces, or background processing within the next year. They are not worth paying when your primary risk is market risk.
If you have sixty days of runway, zero paying users, and an MVP that might pivot three times before finding product-market fit — pick the fastest Next.js starter you can find. Optimize for UI velocity. Accept the architectural debt as a future-you problem.
If you are past that moment: if you have paying customers, a billing model with any complexity, or a roadmap that includes real subscription logic and async jobs — the week you would spend setting up the correct architecture is already spent.
Shape A (Next.js, colocated)Shape B (Refract, split backend)Weeks 1–4, single client, schema in fluxFaster — less setup overheadComparable — boilerplate is pre-wiredFirst billing featureFast if Stripe Checkout sufficesFast if real subscription logic is requiredBackground jobsStructural awkwardness — bolted on outside the frameworkFirst-class typed processes sharing domain layerSecond client surfaceManual type duplication or driftSchema already serves multiple consumersPricing experiment touching 3+ layersRegression risk — implicit contracts across handlersCompiler catches drift before deployAI agent editing the codebaseConvention-dependent — agents follow the path of least resistanceStructural — import boundaries enforce correct layerPrimary riskMarket — will anyone payTechnical — billing correctness, entitlement integrity
Where to go from here
Read the if you want to see how the deployables connect. Read the if you want the full inventory with file paths and configuration details. Read the doc if you want to understand how money flows through the system. If you are already committed to GraphQL as the product API, the names the contenders worth auditing.
The goal of this post is not to convince you that Next.js is wrong. It is a strong framework and the right default for most products at most stages.
The goal is to give you a vocabulary — Shape A and Shape B — precise enough to make the decision deliberately rather than by default. Most TypeScript SaaS starters do not tell you which shape they are. They should.
If you are building a product where billing, RBAC, and background jobs are first-class features and not bolt-ons, you are building Shape B. The question is whether you want to pay the setup cost now or later.
Later means under pressure. Now means it is already done.
FAQ
- What is the difference between Shape A and Shape B?
Shape A prioritizes rendering speed and rapid product iteration, making it ideal for MVPs. Shape B prioritizes explicit API contracts, domain boundaries, and long-term maintainability for products with complex business logic.
- Is Next.js a bad choice for SaaS applications?
No. Next.js is an excellent choice for many SaaS products, especially early-stage startups where UI velocity and fast iteration matter more than architectural complexity.
- When should I move to a split Node.js backend?
Consider it once your product relies on multiple client applications, subscription billing, RBAC, asynchronous jobs, or business rules that must remain consistent across different entry points.
- Why choose GraphQL over tRPC or REST?
For products with multiple consumers, GraphQL provides an explicit schema, end-to-end code generation, and compile-time guarantees that help prevent contract drift as the API evolves.
- Does a Node.js monorepo slow development?
Initially, yes: it introduces more moving parts. But for products with growing domain complexity, the upfront investment often reduces maintenance costs and regression risk over time.