Node is our default for product backends, mostly for a boring reason: the same people can move between the API and the interface without changing language, and the contract between them can be one shared type instead of two hopeful ones.
What we build on it
HTTP and GraphQL APIs, webhook receivers, integration layers between systems that were never designed to meet, scheduled jobs, and the queue workers behind anything a user should not have to wait for.
Modules with edges
A backend becomes unmaintainable when every file can import every other file. We split by domain — billing, accounts, catalogue — and let those talk through defined interfaces. It costs a little discipline early and it is the reason a feature can later be extracted or replaced without a rewrite.
Work that belongs in a queue
Sending mail, generating documents, calling a slow third party, recalculating a report: none of that belongs inside the request that triggered it. It goes to a queue with retries, a dead-letter path and an alert when the backlog grows. We use Redis for that in most projects.
Failure is designed, not discovered
Every outbound call has a timeout. Every retry is idempotent or it is not a retry. Every error that reaches a user is a message someone wrote deliberately. Logs carry a request id all the way through, so a support ticket can be traced instead of reproduced.
Honest limits
Node is a poor fit for sustained CPU-heavy work — image pipelines, large numerical jobs, anything that pins a core for seconds at a time. That work goes to Python or Go as a separate service, and we say which parts we would split before we start.
Typed throughout with TypeScript; deployed by the pipelines under CI/CD.