I would still start a new backend in Node.js. Here’s when I wouldn’t.

Elena Vasquez

Elena Vasquez

August 28, 2026

I would still start a new backend in Node.js. Here’s when I wouldn’t.

The last time a founder asked me what to start a backend in, they said it like they were already apologizing. “I know Node is… old.” They had a TypeScript frontend, two contractors who only write React, a Stripe webhook they needed live by Thursday, and a slide that mentioned “maybe Go later.” I told them to start in Node. I would still tell them that in 2026.

I would also have told the last payments team I sat with to stop putting PDF generation on the request thread. That is the whole article. Node is still the default I reach for when the product is an API, a queue, and other people’s HTTP. It is not the default when the product is a CPU, a hard latency budget, or a shop that already has a runtime they can staff at 3 a.m.

People keep asking whether Node.js is already a legacy default. That is a fair industry question. This is the project question: would I open a new repo in it on Monday.

What I actually mean by “start in Node”

I do not mean a 2016 Express app with var, a global db, and a single routes.js that imports the universe. I mean TypeScript, a current LTS, Fastify or Hono, and a worker process that is allowed to exist from day one. If the company is already a Nest shop, I will start in Nest. I will not start greenfield Nest to feel enterprise, Deno to feel modern, or Bun because a benchmark tweeted a number.

The runtime is Node. The language is TypeScript. The deploy is usually a container or a pair of them: one for HTTP, one for jobs. Postgres is next to it. Redis is not required on day one — I have watched teams add it because a tutorial did. That stack is boring. Boring is the point. A new backend’s first job is to accept money, accept a webhook, write a row, and survive a Friday deploy. Node is still unusually good at that if your team already thinks in JavaScript.

Why it is still my default

The honest reason is not “the event loop is elegant.” The honest reason is that the same people who will write the dashboard can read the webhook handler. I have hired for that. I have also hired for a Go API sitting next to a React app and spent the first quarter translating types by hand, arguing about date formats, and watching the frontend team treat the OpenAPI file like a rumor.

Shared language is a tax you stop paying. When the contractor who built the settings page can open the billing route and see why a coupon failed, you ship the fix the same afternoon. When that route is in a language they do not read, the fix becomes a ticket and a standup. I have lived both weeks. On a six-person team I know which one I will buy again.

Hiring is the second reason, and it is less pretty. In 2026 I can still find a mid-level TypeScript engineer who has seen Fastify, Prisma or Drizzle, and a queue. I can find a strong Go engineer. I cannot always find them in the same city, at the same rate, on a two-week clock. Node’s hiring pool is messy and full of people who only ever wrote Next.js API routes. It is also deep. For a first backend, depth beats purity.

The third reason is the I/O shape of most products. A typical SaaS backend in the first year parses JSON, checks a session, talks to Stripe, Postgres, and an email provider, then fans out a job. That is waiting. Node is built to wait without opening a thread per request. I have seen p99s on a modest box that were fine for a product that had not found its second thousand users. I have also seen people rewrite that box into Go before they had those users. The rewrite did not find them.

Ecosystem is the fourth. npm is still a junk drawer, and supply-chain scares are real. I pin versions and I do not install a package because the name is cute. Even with that, the adapters exist. If a vendor has one first-class SDK, it is still often the JavaScript one. That matters on week two, when you are not writing a client from a curl example.

Laptop in a dim server room showing API logs and latency graphs next to a rack of production machines

The Node I will defend in a design review

If the product is a JSON API with auth, webhooks, and a job queue, I will defend Node without blinking. If the frontend is already TypeScript, I will defend it harder. If the team is three people and one of them is “full-stack,” I will defend it the hardest.

I will also defend it for glue: internal tools, admin APIs, BFF layers in front of something older, the service that signs URLs and mints tokens. I have replaced more Java “integration services” with a 400-line Fastify app than I am proud of. The Java was correct. It was also a two-week change request for a header.

What I will not defend is “Node can do anything if you add workers.” Yes, you can. Worker threads, a separate process, a child that shells out to ffmpeg. Sometimes that is cheaper than a rewrite. Sometimes it is how you discover you needed a rewrite six months ago.

When I would not: the CPU is the product

I would not start a new backend in Node if the request does real work on the CPU before it can answer. Image pipelines. Video. Audio. PDF generation that is not “fill a template.” Crypto that is not “call a library once.” Bulk CSV transforms on a few hundred megabytes. Anything that looks like a data job wearing an HTTP costume.

I watched a Node service spend 800ms in sharp on a thumbnail route and then we argued about clustering. Clustering was not the fix. The fix was a worker in a language that likes bytes, or a managed image service, or both.

I made this mistake on a document product. We put LibreOffice in a container and called it from Node because “the API is already Node.” The API was fine. The conversion queue was a crime scene: memory spiked, a sync slip stalled the loop, and a “quick” export blocked a checkout webhook on the same process. We split the processes, then rewrote the converter. If I were starting that product now, the HTTP API could still be Node. The thing that eats files would not be. Same rule for local model inference: calling a remote model is waiting, and Node is fine. Loading weights next to the API is not.

When I would not: latency is a contract

There is a class of backend where “usually 40ms” is not a product. Market data. Bidding. Multiplayer tick loops. A control plane that sits in front of something that already has a budget. I would not start those in Node.

Not because Node cannot be fast. Fastify can be very fast for JSON. The problem is the tail: garbage collection, a fat dependency, one accidental JSON.parse of a 20MB payload, one analytics SDK that works on import. The p99 becomes the incident-review story. Go and Rust do not make you a better engineer. They make some of those stories rarer, and the remaining ones look like your code instead of your runtime.

I would also not start Node if the service sits in a fleet that is already a systems language and the on-call does not read JavaScript. I have shipped a clever Node sidecar into a Java shop. I also got the page when it died, because nobody else wanted to learn npm at that hour. I do not do that to a team twice.

Engineer at two monitors comparing a job queue with CPU spikes against TypeScript source

When I would not: the shop already has a language

If I join a company that already runs Spring Boot well, I do not start the next service in Node to “modernize the stack.” I start it in what the on-call already knows, unless the service is a BFF the frontend team will own. Ownership beats fashion. The same rule applies in reverse: if every new service is already Go, I do not sneak Node in because I like Fastify. I might still use it for a one-off admin tool. I will not split the production runtime map for a taste preference.

If the company is a TypeScript house — Next, a design system, shared zod schemas — I will not start the backend in Go because a staff engineer read a latency post. I will start in Node, put a budget on the hot path, and extract later if a profiler says so. Extraction is cheaper when you have users. Premature extraction is how you get two deployables and one customer. PHP shops are the case I used to sneer at. I do not anymore. If Laravel already ships, I am not opening Node to feel 2026. I might open it for a realtime piece. I would not migrate the core for the aesthetic.

Bun, Deno, Go, Rust — and the runtime I am not picking first

I get asked about Bun every quarter. I have run it. I like the test runner and the install speed. I have not started a customer-facing backend on it as the production runtime, because the question I actually have is not “is the HTTP hello-world faster.” It is whether this vendor SDK, this native addon, this OpenTelemetry exporter, and this six-month-old Docker image still behave. For that I still want Node’s boring path. If you are specifically shopping the two, the useful comparison is Bun versus Node for backend APIs. That is a runtime bake-off. This piece is the product decision before it.

Deno is the same conversation with better defaults and a smaller hiring story. I will use it for a script I want permissions on. I will not start a company API on it unless the team is already there. Go is what I pick when the service is a pipe and I care about a small binary — not when the same three people are also writing the React app. Rust is what I pick when I can name the reason in one sentence: this process cannot GC-pause, this parser has to be safe, this edge thing has to be tiny. I have watched a team lose a month to the borrow checker on a CRUD API. They were proud of the binary size. Nobody asked about the binary size.

The hybrid I actually ship

Most of my “I would still start in Node” answers are not “Node forever.” They are “Node for the API, something else if a worker earns it.”

A pattern that has not bitten me: Fastify for HTTP, Postgres, a queue, and a worker that can be rewritten without touching the public routes. The first worker is still TypeScript. It stays TypeScript until a profile says the CPU is the bill. Then that worker becomes Go, or a managed service, and the API stays. What I will not do is put the CPU work back on the request path “just until we scale.” That sentence is how you get a checkout that waits on a PDF. Split early. Rewrite late.

Another hybrid I like: a Node BFF in front of an existing JVM or Go core. The BFF shapes JSON for the screen. The core keeps the ledger. I have seen the opposite — a Node core and a Java BFF — and I still do not understand who approved it.

The checklist I use on day one

I ask five questions. If I get four “yes” answers, I start in Node. If I get two, I do not.

  • Is the first year of this service mostly I/O: HTTP, webhooks, a database, other APIs?
  • Is the team already writing TypeScript, or can I hire that faster than the alternative?
  • Will the same people who write the UI need to read and patch the backend in the first six months?
  • Is there a vendor SDK we cannot afford to rewrite, and is it first-class in JavaScript?
  • Can I name a concrete CPU or latency constraint that exists now, not in a slide about “scale”?

The last question is the trap. Everyone can name a future constraint. Almost nobody can name a present one. “We might do video” is not a reason to start in Rust. “We transcode every upload before the user sees a thumbnail” is. I also ask who gets the page at 3 a.m. If the rotation already lives in one runtime, I do not add a second for a service that is not special. If the answer is “me,” I pick the language I can debug while tired. For me that is still TypeScript on Node.

The mistakes I will not repeat

I will not start in Express because it is “simple.” Express is simple until middleware order becomes a personality. Fastify annoyed me for a week and then it stopped surprising me. Hono is fine when I want a tiny surface. Nest is fine when the team already thinks in modules. I will not start Nest to impress a Java hire.

I will not share a process between “user clicked buy” and “user clicked export 4,000 rows.” That is a process problem Node punishes faster because one sync slip stalls the loop. Split the processes, then argue about languages. I will not install an ORM the size of a framework because I am afraid of SQL, and I will not start three Node services because a diagram said “microservices.” One deployable, two processes if I need a worker. I split when a team or scaling boundary shows up in the calendar.

So when would I still start in Node

A new product. A TypeScript frontend. A team that can hire JS. A backend that talks to other computers more than it crunches. Webhooks, auth, Postgres, a job or two. I will start that in Node in 2026 and I will not feel clever about it. I will feel like someone who wants a checkout live before the metaphor.

I would not start in Node when the CPU is the product, when the tail latency is a contract, when the on-call already has a language and this service is not special enough to split the runtime map, or when I can already name the worker that should never have been JavaScript. Those are the times I have paid for the wrong default.

If you are choosing a runtime to feel current, you are already late. If you are choosing one so the same three people can read the webhook and the button that fired it, Node is still a good Monday morning decision. I would make it again. I would also walk away the moment the work stops looking like waiting.

More articles for you