JavaScript (ES2020+)
You should be comfortable with let/const, arrow functions, destructuring, spread/rest, template literals, and import/export. Node uses the same language as browsers, but without DOM APIs — instead you get fs, http, and process.
A structured eight-phase path to backend competence — written by a production full-stack engineer, with milestones, code examples, and project ideas at every stage.
Node.js tutorials are everywhere, but most jump straight into framework magic without explaining the runtime, async model, or production concerns that separate hobby scripts from APIs users trust. This roadmap orders topics the way I teach junior engineers joining backend teams — foundations first, frameworks second, operations last.
Each phase includes concrete milestones so you know when to move on. The examples reference patterns used on elangodev.com — Express-style route handlers, Postgres with connection pooling, environment-based configuration, and testable service boundaries — not abstract foo/bar snippets with no context.
Eight steps from JavaScript review to production deployment. Expect roughly 4–6 months at a steady pace if you are new to backend development.
Variables, functions, arrays, objects, and ES modules. Node.js is JavaScript on the server — weak JS basics become painful quickly. Jump to section →
Use the current LTS release, understand npm and package.json, and run a hello-world file with node. Jump to section →
Read and write files with fs/promises, resolve paths with path, and emit events with EventEmitter. Jump to section →
Routing, middleware, JSON bodies, status codes, and structured error handling. Jump to section →
Promises, async/await, backpressure, and when to use streams instead of loading entire files into memory. Jump to section →
PostgreSQL with an ORM or query builder, migrations, and connection pooling for production. Jump to section →
Environment variables, input validation, rate limiting, JWT or session auth, and OWASP basics. Jump to section →
Unit and integration tests, logging, health checks, Docker, CI/CD, and graceful shutdown. Jump to section →
Node.js is not a beginner's first programming language. Confirm these skills before Phase 1 — or budget extra time to learn them in parallel.
You should be comfortable with let/const, arrow functions, destructuring, spread/rest, template literals, and import/export. Node uses the same language as browsers, but without DOM APIs — instead you get fs, http, and process.
Navigate directories, run scripts, and read error output in a terminal. On Windows, PowerShell or WSL both work; macOS and Linux use bash or zsh by default.
Know what a URL, request method (GET/POST), status code, and JSON payload mean. Our API Testing Guide walks through these concepts with Postman if you need a refresher. Open guide →
Initialize a repo, commit changes, and push to GitHub. Version control is expected on every professional Node.js project.
Work through each phase in order. Complete the milestone before advancing — rushed learning shows up as production outages later.
Install, run, and understand the Node.js runtime
Node.js is a JavaScript runtime built on Chrome's V8 engine. It runs outside the browser, excels at I/O-heavy workloads (APIs, proxies, CLIs), and uses a single-threaded event loop with a libuv thread pool for blocking work. It is not a framework — Express, Fastify, and NestJS sit on top.
Install the Active LTS version from nodejs.org or use nvm/fnm to switch versions per project. LTS receives security fixes for production; Current is for experimenting with the latest features.
npm init creates a manifest with name, version, scripts, and dependencies. npm install adds packages to node_modules and records semver ranges in package-lock.json for reproducible installs.
node index.js executes a file. npm run dev typically wraps nodemon or tsx for reload during development. Use "type": "module" in package.json for native ES modules, or stick with CommonJS require() in older codebases.
node without arguments opens the REPL for quick experiments. In VS Code, attach the debugger with breakpoints — far more productive than console.log alone.
hello-server.mjs
// package.json: { "type": "module" }
import http from "node:http";
const server = http.createServer((req, res) => {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true, path: req.url }));
});
server.listen(3000, () => {
console.log("Listening on http://localhost:3000");
});Milestone
You can explain the event loop at a high level, create a new project, install a dependency, and run a script that responds to HTTP.
Built-in APIs every Node developer uses daily
Read and write files with fs/promises (prefer promises over callback-style fs). path.join and path.resolve build cross-platform file paths. Understand __dirname equivalents in ES modules via import.meta.url.
process.env holds environment variables. process.argv parses CLI arguments. process.exit and signal handlers (SIGTERM) matter for graceful shutdown in production.
Many Node APIs are event-driven. Custom classes can extend EventEmitter to decouple publishers and subscribers — a pattern used in streams and HTTP servers.
Binary data lives in Buffer objects. Know UTF-8 vs base64 when handling uploads, cryptography, or file transforms.
Spawn shell commands from Node for CLI tools, image processing pipelines, or wrapping legacy binaries. Prefer execFile with argument arrays over string concatenation to avoid injection.
read-config.mjs
import { readFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const configPath = path.join(__dirname, "config.json");
const raw = await readFile(configPath, "utf8");
const config = JSON.parse(raw);
console.log(config.port ?? 3000);Milestone
You can build a small CLI that reads a JSON config, writes log files, and handles errors without crashing silently.
Frameworks, routing, and API design
Express is the most common choice with a huge middleware ecosystem. Fastify prioritizes performance and built-in schema validation. Pick one, learn routing, route params, query strings, and request bodies.
Middleware functions run in order — logging, CORS, body parsing, authentication. Errors propagate to a central error handler that maps exceptions to HTTP status codes and safe JSON messages.
Use nouns for resources (/users, /posts/:id), correct HTTP verbs, and consistent status codes (201 Created, 204 No Content, 404 Not Found, 422 Unprocessable Entity for validation errors).
Validate input with Zod, Joi, or Fastify schemas before touching the database. Never trust client payloads — even from your own frontend.
OpenAPI (Swagger) documents endpoints for frontend teams and external consumers. Generate types from schemas where possible.
express-route.js
import express from "express";
const app = express();
app.use(express.json());
app.get("/api/health", (_req, res) => {
res.json({ status: "ok" });
});
app.post("/api/messages", (req, res) => {
const { email, body } = req.body ?? {};
if (!email || !body) {
return res.status(400).json({ error: "email and body required" });
}
// persist to database...
res.status(201).json({ id: "msg_1" });
});
app.use((err, _req, res, _next) => {
console.error(err);
res.status(500).json({ error: "Internal server error" });
});
app.listen(3000);Milestone
You deploy a CRUD API with validation, pagination, and a global error handler. Practice against the sandbox endpoints in our API Testing Guide. Practice here →
Non-blocking I/O done right
Modern code uses async/await. Understand Promise.all for parallel work and Promise.allSettled when partial failure is acceptable. Avoid unhandled rejections — they can crash Node in strict configurations.
CPU-heavy work on the main thread blocks all requests. Offload to worker_threads, child processes, or a job queue (BullMQ, SQS) when hashing, image resizing, or PDF generation takes hundreds of milliseconds.
Streams process data chunk-by-chunk — essential for large file uploads, CSV parsing, and proxying responses. Pipe with pipeline() from stream/promises to handle errors and backpressure.
setImmediate vs process.nextTick vs setTimeout have different queue priorities. For periodic jobs in production, use a proper scheduler (cron, BullMQ) instead of setInterval in the API process.
stream-copy.mjs
import { createReadStream, createWriteStream } from "node:fs";
import { pipeline } from "node:stream/promises";
await pipeline(
createReadStream("large-input.csv"),
createWriteStream("large-output.csv"),
);
console.log("Copy complete without loading entire file into RAM");Milestone
You can explain why blocking the event loop hurts latency and implement a streaming file upload endpoint.
SQL, ORMs, caching, and migrations
Relational data, indexes, foreign keys, and transactions. PostgreSQL is the default choice for new Node backends — JSONB columns add flexibility without abandoning SQL.
Drizzle and Prisma offer type-safe schemas and migrations. Knex is a lighter query builder. Raw pg with parameterized queries is fine for small services — never interpolate user input into SQL strings.
Each request should borrow a connection from a pool (pg.Pool, Prisma client singleton). Exhausting connections under load is a common production outage — tune pool size and server max_connections together.
Cache expensive reads, store session tokens, or implement rate limiting. Set TTLs explicitly and define cache invalidation when underlying data changes.
Schema changes live in versioned migration files, applied in CI before deploy. Never edit production schema by hand without a rollback plan.
parameterized-query.js
import pg from "pg";
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
export async function findUserByEmail(email) {
const { rows } = await pool.query(
"SELECT id, email, name FROM users WHERE email = $1 LIMIT 1",
[email],
);
return rows[0] ?? null;
}Milestone
You ship a service with migrations, seed data, and at least one transactional workflow (e.g. create order + line items atomically).
Protect users, secrets, and infrastructure
Never commit API keys or DATABASE_URL to git. Use .env locally (gitignored) and platform secrets in production (Vercel, Railway, AWS SSM). Rotate credentials after leaks.
Guard against injection (SQL, NoSQL, command), broken authentication, SSRF, and mass assignment. Use helmet for security headers, cors with explicit origins, and rate limiting on auth routes.
Hash passwords with bcrypt or argon2 — never store plaintext. Session cookies should be httpOnly, secure, and sameSite. For SPAs, JWT access tokens with short expiry plus refresh tokens is a common pattern.
Authentication proves who you are; authorization proves what you can do. Implement role-based or attribute-based checks at the service layer, not only in the UI.
env-check.js
const required = ["DATABASE_URL", "JWT_SECRET", "NODE_ENV"];
for (const key of required) {
if (!process.env[key]) {
throw new Error(`Missing required environment variable: ${key}`);
}
}
// Fail fast at startup — not on the first user requestMilestone
Your API rejects unauthenticated requests, hashes passwords correctly, and passes a basic security checklist (no secrets in repo, HTTPS in production, rate limits on login).
Confidence to refactor without fear
Test pure functions and service modules in isolation. Mock external I/O at boundaries — do not hit real databases in unit tests.
Spin up the Express/Fastify app in-memory and assert HTTP responses. Cover happy paths and common error cases (400, 401, 404).
Use Docker Postgres or a disposable schema per test run. Roll back transactions in tests when possible for speed.
Run lint, typecheck, and tests on every pull request. Block merges on failure. Add coverage thresholds gradually — 100% coverage is not the goal; critical paths are.
supertest-example.js
import request from "supertest";
import { describe, it, expect } from "vitest";
import { app } from "./app.js";
describe("GET /api/health", () => {
it("returns 200", async () => {
const res = await request(app).get("/api/health");
expect(res.status).toBe(200);
expect(res.body.status).toBe("ok");
});
});Milestone
Your project has a test script in package.json, runs in CI, and covers auth plus one core business workflow.
Scale, observe, and ship reliably
Use pino or winston with JSON output. Include request IDs, correlate logs across services, and ship to Datadog, Grafana Loki, or CloudWatch.
Expose /health and /ready endpoints for load balancers. On SIGTERM, stop accepting new connections, drain in-flight requests, close database pools, then exit.
Node cluster module or multiple containers behind a load balancer utilize all CPU cores. Prefer stateless APIs so any instance can serve any request.
Multi-stage Dockerfiles produce small images. Run as non-root. Set NODE_ENV=production. Use platform autoscaling based on CPU or request latency.
clinic.js, 0x, and Chrome DevTools CPU profiles find event-loop blockers. Measure before optimizing — cache database queries and add indexes before rewriting in Rust.
Split services when teams or scaling needs justify operational cost. Start monolithic; extract bounded contexts when deploy coupling hurts velocity.
graceful-shutdown.js
import http from "node:http";
const server = http.createServer(app);
server.listen(3000);
function shutdown(signal) {
console.log(`${signal} received — closing server`);
server.close(() => {
pool.end().then(() => process.exit(0));
});
}
process.on("SIGTERM", () => shutdown("SIGTERM"));
process.on("SIGINT", () => shutdown("SIGINT"));Milestone
You can deploy a containerized API with health checks, centralized logs, and a documented rollback procedure.
Build these at each level to cement skills. Portfolio projects beat certificates when interviewing for backend roles.
CLI file organizer
Read a directory, sort files into folders by extension, log actions to a file. Practices fs, path, and async iteration.
JSON REST API for a todo list
In-memory storage first, then swap to SQLite. CRUD endpoints with validation and proper status codes.
Blog backend with auth
Users register/login, create posts, paginate public feed. PostgreSQL + migrations + JWT or sessions.
URL shortener
Redirect endpoint, click analytics, Redis cache for hot links. Handle collision-safe slug generation.
Webhook processor with queue
Accept webhooks quickly (202 Accepted), enqueue jobs in BullMQ, process with retries and dead-letter queue.
Multi-tenant SaaS API
Tenant isolation in Postgres (row-level security or schema-per-tenant), Stripe billing webhooks, audit logs.
Standard tools on professional Node.js teams. You do not need everything on day one — add categories as you reach the relevant phase.
| Category | Tools |
|---|---|
| Runtime | Node.js LTS, nvm/fnm, corepack (pnpm/yarn) |
| Framework | Express, Fastify, or NestJS for larger teams |
| Database | PostgreSQL, Drizzle/Prisma, Redis |
| Testing | Vitest/Jest, Supertest, Testcontainers |
| Quality | ESLint, Prettier, TypeScript (recommended) |
| Observability | pino, OpenTelemetry, Sentry |
| Deploy | Docker, GitHub Actions, Railway/Fly.io/AWS |
Patterns I see in code reviews and incident postmortems — avoid these early.
Synchronous bcrypt with high rounds, large JSON.parse on megabyte payloads, or tight loops stall every concurrent request. Offload CPU work or use worker threads.
Forgotten await or missing .catch() can crash the process or leave requests hanging. Use eslint-plugin-promise and global unhandledRejection handlers in production.
API keys pushed to GitHub are scraped within minutes. Use environment variables and secret scanners in CI.
Opening a new database connection per request exhausts Postgres max_connections under moderate traffic.
Error handlers should log full details server-side and return generic messages in production responses.
Trusting req.body allows mass assignment, type confusion, and injection attacks. Validate shape and types at the boundary.
Pair this roadmap with other guides on elangodev.com.
Practice HTTP methods, status codes, and Postman against safe sandbox endpoints — essential after you build your first REST API.
Deeper context on serverless architecture, CDNs, caching, and security headers that complement backend Node.js work.
See how open-source Node packages are structured, versioned, and published — a natural next step after CLI and library modules.
Production articles on Next.js API routes, Supabase, S3 uploads, and deployment patterns used on this site.
This roadmap is maintained by Elango P, a full-stack engineer who builds production Node.js and Next.js applications. Content follows our editorial policy — original writing with practical depth, not scraped or auto-generated filler. Questions or corrections? Get in touch.