How to Build a REST API with Node.js and Express in 2026 – The Ultimate Guide
In 2026, Node.js and Express are still the go-to combo for building fast, scalable REST APIs. While newer frameworks exist, Express remains battle-tested, lightweight, and flexible enough for most use cases. If you're shipping APIs today — whether for a startup MVP or a microservice in a larger system — knowing how to do it right with Express is still a core skill.
Let’s cut the fluff and build a real, production-ready REST API from the ground up.
1. Set Up Your Project (Properly)
Start with a clean slate and modern tooling.
mkdir my-api && cd my-api
npm init -y
npm install express
npm install --save-dev nodemon eslint
Use nodemon for development so you don’t restart the server every time. Add scripts to package.json:
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
}
Initialize ESLint with npx eslint --init — pick Airbnb or Standard, doesn’t matter, just pick one and stick to it.
2. Write Your First Server (Minimal but Clean)
Create server.js:
import express from 'express';
const app = express();
const PORT = process.env.PORT || 3000;
// Built-in middleware
app.use(express.json());
app.get('/', (req, res) => {
res.json({ message: 'Welcome to the API' });
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Yes, use ES modules ("type": "module" in package.json) — it’s 2026, and you should be using import/export.
3. Structure Your App Like a Pro
No one wants a 500-line server.js. Use a simple, scalable structure:
src/
├── routes/
├── controllers/
├── models/
├── middleware/
└── utils/
Create a route for users:
// routes/userRoutes.js
import { Router } from 'express';
import { getUsers, createUser } from '../controllers/userController.js';
const router = Router();
router.get('/', getUsers);
router.post('/', createUser);
export default router;
Then mount it in server.js:
import userRoutes from './src/routes/userRoutes.js';
app.use('/api/users', userRoutes);
This keeps things modular and testable.
4. Use Controllers, Not Logic in Routes
Keep routes thin. Business logic goes in controllers.
// controllers/userController.js
let users = []; // Replace with DB later
export const getUsers = (req, res) => {
res.json(users);
};
export const createUser = (req, res) => {
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({ error: 'Name and email required' });
}
const user = { id: users.length + 1, name, email };
users.push(user);
res.status(201).json(user);
};
This is mock data — but the pattern stays the same when you plug in a real database.
5. Add Error Handling Middleware
Don’t let unhandled errors crash your server or leak stack traces.
Create middleware/errorHandler.js:
export const errorHandler = (err, req, res, next) => {
console.error(err.stack);
res.status(500).json({
message: 'Something went wrong!',
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
});
};
Use it at the end of your middleware chain in server.js:
app.use(errorHandler);
Also, handle 404s:
app.use('*', (req, res) => {
res.status(404).json({ error: 'Route not found' });
});
Order matters — error handlers go last.
6. Validate Input Early
Use a lightweight validator. I recommend Zod — it’s type-safe and works great with TypeScript.
npm install zod
// utils/validate.js
import { z } from 'zod';
const userSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
});
export const validate = (schema) => (req, res, next) => {
try {
schema.parse(req.body);
next();
} catch (err) {
return res.status(400).json({ error: err.errors });
}
};
Now use it in your route:
router.post('/', validate(userSchema), createUser);
Validation at the edge prevents garbage from entering your system.
7. Use Environment Variables
Never hardcode config. Use .env:
npm install dotenv
// server.js
import 'dotenv/config';
Create .env:
PORT=5000
NODE_ENV=development
Now process.env.PORT works everywhere.
8. Connect to a Database (Prisma + PostgreSQL)
For 2026, Prisma is still one of the best ORMs — it’s intuitive and type-safe.
npm install prisma pg
npx prisma init
Set up prisma/schema.prisma:
model User {
id Int @id @default(autoincrement())
name String
email String @unique
}
Run npx prisma migrate dev --name init to create the table.
Now update your controller:
// controllers/userController.js
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export const getUsers = async (req, res) => {
const users = await prisma.user.findMany();
res.json(users);
};
export const createUser = async (req, res) => {
const { name, email } = req.body;
try {
const user = await prisma.user.create({ data: { name, email } });
res.status(201).json(user);
} catch (err) {
res.status(400).json({ error: 'Email already exists' });
}
};
Don’t forget to handle DB errors.
9. Add Logging (Don’t Guess, Know)
Use Pino — fast, JSON-based, and production-ready.
npm install pino
import pino from 'pino';
const logger = pino();
// In your routes
logger.info('User created', { userId: user.id });
Pipe logs to your observability stack (Datadog, Grafana, etc.). Local dev? Just use pino-pretty.
10. Secure Your API (Basics)
Even a simple API needs basic security:
npm install helmet cors
import helmet from 'helmet';
import cors from 'cors';
app.use(helmet()); // sets secure headers
app.use(cors()); // allow frontend origins
Also rate-limit public endpoints:
npm install express-rate-limit
import rateLimit from 'express-rate-limit';
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP
});
app.use('/api/', limiter);
11. Test the Damn Thing
Use Jest and Supertest:
npm install --save-dev jest supertest
// test/user.test.js
import request from 'supertest';
import app from '../server.js';
describe('GET /api/users', () => {
it('returns 200 and users', async () => {
const res = await request(app).get('/api/users');
expect(res.statusCode).toBe(200);
expect(Array.isArray(res.body)).toBe(true);
});
});
Run with npx jest. Write tests. Your future self will thank you.
Conclusion
Building a REST API in 2026 with Node.js and Express isn’t about magic — it’s about doing the boring stuff right:
- Structure your code
- Validate input
- Handle errors
- Use a real database
- Log, secure, and test
Express isn’t flashy, but it’s reliable. And when you combine it with modern tooling (Prisma, Zod, Pino), you’ve got a stack that scales and survives real-world use.
Don’t overcomplicate it. Build, ship, iterate.
Now go write some code.
Top comments (0)