Updated Nov 5, 20266 min read

How to Build a Node.js Backend: A Practical Guide

What a production Node.js backend is made of, from the HTTP layer and the database to background jobs, auth and deploys, with TypeScript code for each piece and the choices that matter at each step.

A Node.js backend usually starts as one Express file with a few routes and grows into the thing the whole product depends on: an API, a PostgreSQL database with migrations, a queue for emails and webhooks, a nightly job, authentication, and a deploy pipeline to AWS or GCP. Node.js handles that well, and its event loop is a good match for APIs that mostly wait on databases and other services.

The hard parts sit between those pieces, where each one brings its own library, its own local container and its own cloud resource, so a new developer needs Docker Compose, a .env file and a wiki page before the API answers locally, and production drifts from that setup over time. This guide walks through each layer of a Node.js backend in TypeScript and the choices at each step.

The layers of a Node.js backend at a glance

LayerEncoreCommon Node.js choices
HTTP APIapi() endpoints with typed requests and runtime validationExpress, Fastify, Hono, NestJS
DatabaseSQLDatabase with migrations, Postgres started by encore runpg or postgres.js with Drizzle, Prisma or Kysely
Background jobsTopic and SubscriptionBullMQ on Redis, SQS, RabbitMQ
Scheduled jobsCronJobnode-cron, Cloud Scheduler, EventBridge
AuthauthHandler with typed auth dataPassport, Better Auth, Clerk or Auth0 SDKs
ObservabilityTracing and logs built inOpenTelemetry, pino
DeployYour own AWS or GCP accountDocker image on ECS, Cloud Run, Render or a VPS

Each option is covered below, starting with Encore.

Every layer in one TypeScript codebase with Encore

Encore is a tool that works alongside your framework for TypeScript and Go, with a platform that provisions and deploys into your own AWS or GCP account. Each row in the table above is a primitive you declare in application code, so the API, the database it writes to and the queue it publishes to are defined together and run together locally.

Application code passes through Encore and infrastructure guardrails to resources provisioned in your own AWS or GCP account.
Encore provisions the infrastructure your application needs in your own AWS or GCP account.

A signup service with a database, an event for new users and an idempotent subscriber that sends the welcome email looks like this:

// users/users.ts import { api } from "encore.dev/api"; import { SQLDatabase } from "encore.dev/storage/sqldb"; import { Topic, Subscription } from "encore.dev/pubsub"; const db = new SQLDatabase("users", { migrations: "./migrations" }); interface SignupEvent { userId: number; email: string; } export const signups = new Topic<SignupEvent>("signups", { deliveryGuarantee: "at-least-once", }); export const signup = api( { expose: true, method: "POST", path: "/signup" }, async ({ email }: { email: string }): Promise<{ id: number }> => { const row = await db.queryRow<{ id: number }>` INSERT INTO users (email) VALUES (${email}) RETURNING id`; await signups.publish({ userId: row!.id, email }); return { id: row!.id }; }, ); // At-least-once delivery: claim with a marker, send, and release the marker if the provider fails. const _ = new Subscription(signups, "welcome-email", { handler: async ({ userId, email }) => { const claimed = await db.queryRow<{ user_id: number }>` INSERT INTO welcome_emails (user_id) VALUES (${userId}) ON CONFLICT (user_id) DO NOTHING RETURNING user_id`; if (!claimed) return; try { await sendWelcomeEmail(email); // your email provider call } catch (err) { await db.exec`DELETE FROM welcome_emails WHERE user_id = ${userId}`; throw err; } }, });

The migration in users/migrations creates the users and welcome_emails tables the queries use:

-- users/migrations/1_create_tables.up.sql CREATE TABLE users ( id BIGSERIAL PRIMARY KEY, email TEXT NOT NULL UNIQUE ); CREATE TABLE welcome_emails ( user_id BIGINT PRIMARY KEY );
Get started
brew install encoredev/tap/encore &&
encore app create
Copy

Run in your terminal to get started locally.

encore run starts Postgres locally, applies the migrations and runs the topic and subscriber on your machine, and the local dashboard shows a trace for each request across the endpoint, the SQL queries and the message handling. Cron jobs are declared the same way with new CronJob, though they do not run locally and are triggered in deployed environments. On deploy, Encore provisions the matching databases and Pub/Sub resources in your account, RDS and SNS/SQS on AWS or Cloud SQL and Pub/Sub on GCP, runs the services on Fargate, EKS, Cloud Run or GKE, and provisions an IAM role for the service. For AI coding agents this layout keeps a feature in one change: the agent adds a table, an endpoint and a subscriber, runs them with encore run, and the deploy creates whatever infrastructure the new code declares.

The HTTP layer

Common choices are Express for its ecosystem, Fastify for speed and JSON schema validation, Hono for a small footprint that also runs on edge runtimes, and NestJS for a structured, decorator-based architecture. Whichever you choose, write it in TypeScript with strict mode on, and validate request bodies at runtime with a library such as Zod, since TypeScript types are erased before the code runs. The Node.js backend frameworks guide compares the options in detail.

The database

PostgreSQL is the default choice for a new backend, and in Node.js you connect with node-postgres or postgres.js, usually through a query builder or ORM such as Drizzle, Prisma or Kysely, and keep schema changes in versioned migration files that run before the new code starts. Use a connection pool sized to the database's connection limit across all instances, not per instance, and parameterized queries everywhere.

Background and scheduled jobs

Anything slow or unreliable, such as sending email, calling third-party APIs or processing uploads, belongs outside the request. Queues such as BullMQ on Redis, Amazon SQS or Google Cloud Pub/Sub deliver messages at least once, which means every consumer must be safe to run twice, as in the ON CONFLICT DO NOTHING pattern above. Scheduled work like nightly cleanups runs from node-cron inside one instance, or better from a cloud scheduler, so that scaling to three instances does not run the job three times.

Auth, config and observability

Validate sessions or tokens once in middleware and hand handlers a typed user object, and check permissions per resource in the handler. Keep configuration in environment variables and secrets in a secret manager rather than a committed .env file. Log structured JSON with a library such as pino, and add OpenTelemetry tracing early, since following one request across an API, a queue and a worker is hard to retrofit.

Good to know

Node.js runs JavaScript on a single thread, so a CPU-heavy loop blocks every other request on that instance; move that work to worker threads or a separate service. Use an active LTS release of Node.js in production, and handle SIGTERM by closing the server and draining connections so deploys do not drop requests.

How to choose your stack

For a small API with one database, a framework such as Fastify or Express with a query builder and a Docker image is a reasonable start. Once the backend adds queues, scheduled jobs and a second service, each new piece adds local setup and cloud configuration that has to be kept in sync.

Pick Encore when you want the API, database, queues and cron jobs of a Node.js backend declared in one TypeScript codebase, running locally with encore run and deployed to your own AWS or GCP account.

Frequently asked questions

What do I need to build a Node.js backend?

A Node.js backend needs an HTTP layer for its API, a database with migrations, a way to run background work such as queues and scheduled jobs, authentication, configuration and secrets, logging and tracing, and a deploy target. TypeScript and PostgreSQL are a common default for the language and the main database.

Is Node.js good for backend development?

Yes, Node.js suits I/O-heavy backends such as APIs, realtime services and integrations, since its event loop handles many concurrent requests on one thread. CPU-heavy work like image processing or large computations should move to worker threads or a separate service.

How do I build a Node.js backend with Encore?

Install the Encore CLI, create an app with encore app create, and define endpoints with api from encore.dev/api, databases with SQLDatabase, and background work with Topic, Subscription and CronJob. Run everything locally with encore run, which starts Postgres for you, and deploy to your own AWS or GCP account.

Deploy to your cloud on AWS and Google Cloud
without managing infrastructure

Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.

~/orders — claude
Claude Code
Claude Code v2.1.180Opus 5.5~/orders