The stack I actually ship with.
Five years and 70+ products taught me which patterns hold up at 10K users and which ones collapse at 100K. This category covers full-stack engineering that's been battle-tested in production — not framework demos written the day a new library was released. The line between "tutorial code" and "production code" is enormous, and most of the gap is invisible until you cross it.
I write about React, Next.js, Node.js, Express, and MongoDB because those are the tools I reach for daily — not because I think they're the best in some abstract sense. There are better choices for specific problems. There are no better choices for the median full-stack project shipped by a small team on a deadline. The posts here cover the patterns I trust, the patterns I avoid, and the migrations that taught me the difference.
Why this category exists
Most full-stack content online is written for the first month of a project. The Next.js tutorial that gets you to "Hello World" in fifteen minutes. The React state management article that solves a problem you don't yet have. The MongoDB schema design post that ignores the question of indexes. They're not wrong, but they leave out everything that matters once your project is six months in and has real users.
The posts in this category live in the next phase. Your app is up, customers are using it, and now you have to decide whether to introduce a queue or scale your existing pattern, whether to denormalize your data or wait for the slow query log to scream, whether to migrate to TypeScript now or in two years. Those are the decisions that actually shape engineering careers. They're rarely covered well online because they're hard to write about without specific context. I try to provide that context.
What you'll find here
The posts in this category cover five broad areas: framework-level patterns for Next.js and React, backend architecture with Node.js and MongoDB, the boring-but-critical stuff (auth, payments, billing), TypeScript adoption at real scale, and migration stories.
- Next.js App Router patterns — server components, streaming, partial pre-rendering, route handlers, edge runtime tradeoffs, and how to actually structure a Next.js app that grows past 50 routes
- React performance — when to memoize, when not to, when state lifting is wrong, when to give up and rewrite, and how to read a React Profiler trace without lying to yourself
- Backend architecture — Node.js + Express + MongoDB patterns that survive scale, when to introduce queues, when to split services, when to stay monolithic
- Auth, payments, and billing — the boring but load-bearing layer most tutorials skip, including Stripe and Razorpay patterns, multi-tenant data isolation, and audit logging
- TypeScript at scale — adoption strategies that don't break the team, when "as any" is correct, and the migration playbook from JS to TS without halting feature work
- Migration stories — moving from legacy stacks to modern, from Express to Next.js Route Handlers, from MongoDB to Postgres, from monolith to micro-services and back
Next.js App Router in production: what actually changes
The App Router got a lot of justified criticism in its first 18 months — confusing caching, opaque error messages, and a steep ramp from Pages Router for established teams. Most of those rough edges are now fixed, and the production payoff is real: server components reduce the JS shipped to the client by a meaningful amount, streaming improves perceived performance, and route handlers replace a lot of awkward `pages/api` plumbing.
The thing that takes longest to internalise is the caching model. By default Next.js will cache aggressively, which is great for performance and dangerous for correctness. Most production bugs I've seen in App Router apps trace back to "the cache wasn't invalidated when I changed the data." The posts in this category walk through the specific caching primitives — `cache`, `revalidatePath`, `revalidateTag`, the fetch cache — with the mental model I've found actually works for explaining to teammates.
Server Actions are the other significant shift. They make form handling dramatically simpler when used correctly and dramatically more confusing when used wrong. The pattern I've converged on: server actions for mutations, route handlers for anything API-shaped, hard rule about boundaries between them. Posts here cover the structure of larger codebases where this pattern has held up across multiple teams.
React performance: the part that's actually subtle
The first React performance advice anyone gives is "memoize everything." It's bad advice. `useMemo` and `React.memo` have a cost — both in code complexity and in actual runtime overhead — and the savings are often zero or negative on components that don't render often. The real performance work is upstream: keep state local, keep components small, avoid re-rendering trees unnecessarily.
The single highest-leverage React performance technique I've seen used in production is moving state down the tree. A piece of state lifted three layers above its consumer causes those three layers to re-render every time it changes. Moving the state to its actual consumer (or to context, or to a state library) often improves performance more than dozens of `useMemo` calls scattered across the codebase. Posts here cover the specific decision framework I use for "where should this state live."
The second technique nobody talks about: virtualisation for long lists. The moment your list exceeds 100 items rendered at once, you're paying performance for nothing. Libraries like `react-virtual` or `react-window` are not optional optimisations — they're the difference between an interactive app and a frozen tab. Posts here cover the integration patterns and the surprising places virtualisation is needed.
The third: profiling your app before optimising it. The React DevTools Profiler is genuinely good. Run it. Find the slow renders. Fix them. Don't guess. Half the "performance fixes" I've seen reviewed were optimising components that weren't even on the slow path.
Node.js + MongoDB architecture that survives scale
The default Node.js + MongoDB project structure that tutorials show — a single Express app, controllers calling Mongoose models, no abstraction in between — works fine up to maybe 50K users. After that, things start to fray. Database connections spike during traffic peaks, slow queries pile up, and the dependency graph between controllers and models becomes hard to reason about.
The pattern I trust at scale: a clear repository layer between controllers and models, with the repository owning all query construction. This sounds like architecture astronaut nonsense until the day you need to add caching, or audit logging, or read-replica routing — at which point you can add it in one place instead of fifty. Posts here cover the specific repository pattern shape I've used, with worked examples.
MongoDB-specific advice that I wish someone had told me earlier: index every query path, not just the obvious ones. The slow query log is your friend; turn it on early. Aggregation pipelines are powerful but bring their own performance traps. Posts here cover the patterns for designing schemas that work both for transactional queries and for analytics, the moment you have to do both at once.
The queue question: introduce one early. Bull, BullMQ, or AWS SQS — any of them are fine. The wrong answer is "we'll add a queue later when we need it" because by the time you need it, you'll be in the middle of an outage. Anything that takes more than 200ms in a request handler should probably be a queue job; designing for that from the start saves rewrites.
The boring layer: auth, payments, billing
If there's one piece of advice I'd give a junior engineer starting their first SaaS, it's "don't underestimate the auth and billing layer." Tutorials skip it because it's not glamorous. Founders downplay it because it's not differentiating. But it accounts for an enormous share of the bugs, the security incidents, and the support tickets in any real SaaS product.
The auth patterns I trust: NextAuth (now Auth.js) for most cases, Clerk when the team wants managed and can pay, custom-built only when there are genuinely unique requirements. Stripe for international payments, Razorpay for India-first, both when serving both markets. Multi-tenant data isolation enforced at the database query level, not just at the application layer — defence in depth matters here more than anywhere else.
The billing patterns that took me too long to learn: subscription state belongs in your database, not in Stripe's. Webhooks are unreliable and need a reconciliation job. Trial states, grace periods, downgrade-on-cancel — these need to be in your data model from day one or you'll spend months rewriting your billing logic later. Posts here cover the specific billing state machine I've used across multiple SaaS launches.
TypeScript adoption: the migration playbook that doesn't break velocity
The advice to adopt TypeScript is universal at this point and basically correct, but the adoption process is where teams trip. The two failure modes I see most often: stopping at "JavaScript with types" (using `any` everywhere, not getting the actual benefits), or grinding to a halt during the migration because the team tried to convert everything at once.
The migration playbook that's worked across multiple teams I've worked with: start with `allowJs: true` and `strict: false`. Rename one file at a time from `.js` to `.ts`, fixing the type errors as you go. Don't change the runtime behaviour — just satisfy the compiler. New code is always TypeScript from day one. Old code gets migrated opportunistically when it's being modified for other reasons.
Once a meaningful portion of the codebase is in TypeScript (50%+), flip `strict: true` for new files using `tsconfig.json` overrides. This is the moment where the actual benefits kick in — strict TypeScript catches real bugs, loose TypeScript catches mostly syntax errors. Posts here cover the specific `tsconfig.json` shapes I've used, with the per-folder strictness overrides that let you migrate progressively without halting feature work.
The patterns that actually move the needle: discriminated unions for state, branded types for IDs (preventing accidental swaps), exhaustive switch checks with `never`, and the pragma about when to use `unknown` vs `any`. Posts here cover each with examples from real codebases, including the cases where loose typing is correct (escape hatches at system boundaries, for instance).
Common mistakes I see (and have made)
The patterns of failure repeat across projects:
- No database indexes until queries get slow, then a panicked index addition during an incident
- State management framework overkill — reaching for Redux on a five-component app, then drowning in boilerplate
- API endpoints that take forever — synchronous calls to external services in request handlers, blocking the whole server during slow third-party APIs
- Frontend state duplicating server state — building elaborate client caches when React Query or SWR solve the problem in three lines
- Migrations as a side script — running migrations by hand on prod because the tooling was never set up properly
- No environment parity — dev environment that bears no resemblance to production, leading to "works on my machine" reaching prod weekly
- No load testing — discovering breaking points during actual user traffic instead of in a controlled test
Each of these gets a dedicated post here over time. The pattern is always the same: the issue feels small until it's not, and the cost of fixing it grows roughly with the square of how long you waited.
What's coming next in this category
The next few posts on the docket: a deep-dive into the Next.js App Router caching model with the mental model that finally made it click for me, a benchmark comparing React Query vs SWR vs raw fetch in real production conditions, a write-up of the repository pattern shape that's held up across four different Node.js codebases, and a post on the MongoDB schema migrations workflow I use without downtime. There's also a long-overdue piece on the specific TypeScript adoption strategy that didn't break a team's velocity.
If there's a specific full-stack problem you're stuck on, the contact form on the homepage works. Reader-driven topics are usually the best ones — they push the writing toward genuinely useful specifics instead of comfortable generalities, and the resulting posts almost always rank better because they answer questions people are actually searching for.
