{"post":{"slug":"how-to-build-a-rest-api-in-node-2026","title":"How to build a REST API in Node in 2026: the complete guide","excerpt":"Building a production-ready REST API in Node.js in 2026 means choosing the right framework, structuring routes cleanly, and shipping validation and auth from day one.","category":"code","date":"March 11, 2026","author":"ByteLedger Team","content":"<span class=\"dropcap\">B</span>uilding a REST API in Node.js used to mean Express and a lot of copy-pasted middleware. In 2026, the ecosystem is more opinionated: Fastify dominates new projects, TypeScript is default, and Zod handles validation cleanly at the schema level. This guide builds a production-grade API from scratch.\n\n## What changed in 2026\n\n- **Fastify v5** is stable and ships with native TypeScript types, built-in JSON schema validation, and ~35k req/s on a single core — roughly 2× Express on benchmarks.\n- **Zod 4** cut bundle size by ~50% and is the standard for TypeScript-first validation.\n- **Node 22 LTS** includes native `fetch`, `WebSocket`, and a built-in test runner, removing several common dependencies.\n- **Hono** emerged as the ultra-lightweight alternative (runs on Workers/Deno/Bun) for edge deployments; use it if you're targeting Cloudflare Workers.\n\n## Project structure\n\n```\nsrc/\n  app.ts           # Fastify instance + plugin registration\n  server.ts        # Entry point — listen()\n  routes/\n    users.ts\n    posts.ts\n  services/\n    user.service.ts\n  db/\n    client.ts      # Postgres/Prisma/Drizzle instance\n  schemas/\n    user.schema.ts # Zod schemas\n  middleware/\n    auth.ts\n```\n\nKeep routes thin. All business logic lives in `services/`. All DB access lives in `db/` or through an ORM. This separation makes testing straightforward.\n\n## Bootstrap with Fastify\n\n```ts\n// src/app.ts\nimport Fastify from 'fastify';\nimport { userRoutes } from './routes/users.js';\n\nexport function buildApp() {\n  const app = Fastify({ logger: true });\n\n  app.register(userRoutes, { prefix: '/api/users' });\n\n  app.setErrorHandler((error, _req, reply) => {\n    app.log.error(error);\n    const statusCode = error.statusCode ?? 500;\n    reply.status(statusCode).send({\n      error: error.message ?? 'Internal Server Error',\n    });\n  });\n\n  return app;\n}\n```\n\n```ts\n// src/server.ts\nimport { buildApp } from './app.js';\n\nconst app = buildApp();\nawait app.listen({ port: 3000, host: '0.0.0.0' });\n```\n\n## Route with validation\n\n```ts\n// src/routes/users.ts\nimport { z } from 'zod';\nimport type { FastifyPluginAsync } from 'fastify';\nimport { createUser, getUserById } from '../services/user.service.js';\n\nconst CreateUserBody = z.object({\n  email: z.string().email(),\n  name: z.string().min(1).max(100),\n});\n\nexport const userRoutes: FastifyPluginAsync = async (app) => {\n  app.post('/', async (req, reply) => {\n    const body = CreateUserBody.parse(req.body); // throws ZodError on bad input\n    const user = await createUser(body);\n    return reply.status(201).send(user);\n  });\n\n  app.get('/:id', async (req, reply) => {\n    const { id } = req.params as { id: string };\n    const user = await getUserById(id);\n    if (!user) return reply.status(404).send({ error: 'User not found' });\n    return user;\n  });\n};\n```\n\nFor Zod errors, add a preHandler or error handler that maps `ZodError` to 400 responses with field-level detail.\n\n## HTTP status codes that matter\n\n| Situation | Status |\n| --- | --- |\n| Resource created | 201 Created |\n| Bad request / validation error | 400 Bad Request |\n| Missing auth | 401 Unauthorized |\n| Authenticated but forbidden | 403 Forbidden |\n| Resource not found | 404 Not Found |\n| Duplicate / conflict | 409 Conflict |\n| Server error | 500 Internal Server Error |\n\nReturn the status code. Do not return 200 for everything and hide the real status in the body.\n\n## Authentication middleware\n\n```ts\n// src/middleware/auth.ts\nimport type { FastifyRequest, FastifyReply } from 'fastify';\nimport { verifyJwt } from '../lib/jwt.js';\n\nexport async function requireAuth(req: FastifyRequest, reply: FastifyReply) {\n  const token = req.headers.authorization?.replace('Bearer ', '');\n  if (!token) return reply.status(401).send({ error: 'No token' });\n\n  const payload = verifyJwt(token);\n  if (!payload) return reply.status(401).send({ error: 'Invalid token' });\n\n  req.user = payload; // augment FastifyRequest type\n}\n```\n\nRegister it per-route or per-plugin with `preHandler: [requireAuth]`.\n\n## How to pick your stack\n\n1. **Fastify + Zod + Drizzle ORM** — type-safe end to end, fastest Node option, good for new projects.\n2. **Express + Zod + Prisma** — more ecosystem examples, slightly lower perf, still fine for most loads.\n3. **Hono + Drizzle** — if you're deploying to Cloudflare Workers or Deno.\n4. **NestJS** — structured like Angular; choose it if your team needs the opinionated module system, avoid it if you want simplicity.\n\n## Common mistakes\n\n**Putting DB queries in route handlers.** One refactor away from untestable spaghetti. Services call DB; routes call services.\n\n**No pagination on list endpoints.** Every list endpoint needs `limit` + `cursor` or `offset` before it hits production. See [Pagination: cursor vs offset in 2026](/blog/pagination-cursor-vs-offset-2026).\n\n**No rate limiting.** Add `@fastify/rate-limit` before you go live. See [How to rate limit an API in 2026](/blog/how-to-rate-limit-an-api-2026).\n\n**Swallowing errors silently.** `catch(e) {}` means failures vanish. Always log and rethrow or return an appropriate status.\n\n**Not versioning the API.** Prefix routes with `/api/v1/` from day one — you will need v2 eventually.\n\n## What to skip\n\n- **Express for new projects** unless you have specific ecosystem reasons. Fastify ships with what Express makes you install separately.\n- **Rolling auth from scratch** — use a proven JWT library (`jose`, `@fastify/jwt`) or a managed provider.\n- **Returning raw DB rows** to clients — always serialize through a schema to avoid leaking internal fields.\n\n## FAQ\n\n**Express or Fastify in 2026?**\nFastify for new projects. Express for projects where the team knows it and performance isn't a concern.\n\n**Should I use TypeScript?**\nYes. Fastify's TypeScript support is first class, and type errors at build time are far cheaper than 400s in production.\n\n**How do I handle file uploads?**\nUse `@fastify/multipart`. Stream to S3 or R2; do not buffer the whole file in memory.\n\n**How do I add OpenAPI/Swagger docs?**\n`@fastify/swagger` + `@fastify/swagger-ui` auto-generate from your JSON schemas or Zod schemas via `fastify-type-provider-zod`.\n\n## Where to go next\n\nSee [How to set up OAuth login in 2026](/blog/how-to-set-up-oauth-login-2026), [How to rate limit an API in 2026](/blog/how-to-rate-limit-an-api-2026), and [How to set up Postgres locally in 2026](/blog/how-to-set-up-postgres-locally-2026).","hue":318,"metaTitle":"How to Build a REST API in Node 2026","metaDescription":"Complete 2026 guide to building a REST API in Node.js with Fastify and Zod. Covers project structure, validation, auth, error handling, and deployment.","keywords":["how to build a rest api in node 2026","node.js rest api tutorial 2026","fastify rest api","node api with typescript","express vs fastify 2026","rest api project structure","zod validation node","node api authentication","rest api error handling","node.js api deployment 2026"],"tldr":["**Fastify outperforms Express** on throughput and has first-class TypeScript and JSON schema validation built in — use it for new projects in 2026.","**Validate every request body at the boundary** — Zod or JSON Schema stops bad data before it reaches your database.","**Use a layered architecture** — route handler → service → data access; never query the DB directly from a route handler.","**Return proper HTTP status codes** — 201 for creation, 400 for bad input, 404 for missing resources, 409 for conflicts.","**Skip:** putting business logic inside middleware. Middleware is for cross-cutting concerns; keep domain logic in service functions."],"readTime":"4 min read","wordCount":913},"related":[{"slug":"how-to-build-a-rest-api-2026","title":"How to build a REST API in 2026: the complete guide","excerpt":"REST APIs are still the backbone of most web backends in 2026. This guide covers design, implementation, validation, auth, and deployment correctly.","category":"code","date":"April 14, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":322},{"slug":"how-to-build-an-api-in-python-2026","title":"How to build an API in Python in 2026: FastAPI, Pydantic, and deployment","excerpt":"FastAPI remains the go-to for Python APIs in 2026, with Pydantic v2 validation and async-first design. This guide covers structure, auth, and real deployment steps.","category":"code","date":"March 28, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":319},{"slug":"express-vs-fastify-2026","title":"Express vs Fastify in 2026: which Node.js framework should you use?","excerpt":"Express is the ubiquitous Node.js default; Fastify is the modern high-performance contender. Here is how they compare for real projects in 2026.","category":"code","date":"April 13, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":312}],"sameCategory":[{"slug":"bazel-build-explained-2026","title":"Bazel Explained: What Hermetic Builds Actually Cost","excerpt":"Bazel delivers reproducible, precisely cached builds and demands that you describe everything. Here is what you get, what it costs, and who should say no.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":145},{"slug":"graphql-federation-2026","title":"GraphQL Federation: One Graph, Many Teams","excerpt":"Federation composes independently-owned subgraphs into a single API. Here is what it solves, the operational cost it adds, and when a simpler gateway is enough.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":300},{"slug":"hermetic-builds-explained-2026","title":"Hermetic Builds Explained: Same Inputs, Same Output","excerpt":"A hermetic build depends only on declared inputs. Here is what that property gives you, the common leaks that break it, and how to move toward it incrementally.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":130},{"slug":"monorepo-tooling-compared-2026","title":"Monorepo Tooling Compared: What You Actually Need","excerpt":"Monorepo tools solve task orchestration and caching, not repository layout. Here is what each tier provides and how to tell when you have outgrown the simple option.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":150}],"newer":{"slug":"how-to-build-a-reading-habit-2026","title":"How to build a reading habit in 2026: a system that actually sticks","excerpt":"Most reading habits fail because they start with motivation instead of structure. This 2026 guide shows you a system that makes reading automatic.","category":"guides","date":"March 11, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":125},"older":{"slug":"how-to-build-a-telegram-bot-2026","title":"How to build a Telegram bot in 2026","excerpt":"Telegram bots use the Bot API over webhooks or long-polling. This guide covers a production-ready setup with grammY in 2026, including commands and middleware.","category":"code","date":"March 11, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":306}}