
Building a REST API with Node.js: Complete Guide for 2025
Building a REST API with Node.js: Complete Guide for 2025
REST APIs power the modern web. From mobile apps to microservices, understanding how to build robust, secure, and scalable APIs is an essential skill for any developer. This comprehensive guide walks you through building production-ready REST APIs with Node.js.
What is a REST API?
REST Principles
| Principle | Description |
|---|---|
| Stateless | Each request contains all information needed; server stores no client state |
| Client-Server | Separation of concerns between frontend and backend |
| Cacheable | Responses can be cached to improve performance |
| Uniform Interface | Consistent way to interact with resources |
| Layered System | Client doesn't know if connected directly to server |
Project Setup
Let's build a complete REST API for a task management system. We'll use:
- Node.js - Runtime environment
- Express.js - Web framework
- TypeScript - Type safety
- PostgreSQL - Database
- Prisma - ORM
- Zod - Validation
- JWT - Authentication
Initialize the Project
# Create project directory
mkdir taskflow-api && cd taskflow-api
# Initialize Node.js project
npm init -y
# Install dependencies
npm install express cors helmet morgan dotenv bcryptjs jsonwebtoken
npm install @prisma/client zod express-async-errors
# Install dev dependencies
npm install -D typescript ts-node-dev @types/node @types/express
npm install -D @types/cors @types/bcryptjs @types/jsonwebtoken
npm install -D prisma eslint prettier
# Initialize TypeScript
npx tsc --init
# Initialize Prisma
npx prisma init
Project Structure
taskflow-api/
โโโ src/
โ โโโ config/
โ โ โโโ database.ts
โ โโโ controllers/
โ โ โโโ auth.controller.ts
โ โ โโโ tasks.controller.ts
โ โโโ middleware/
โ โ โโโ auth.middleware.ts
โ โ โโโ error.middleware.ts
โ โ โโโ validate.middleware.ts
โ โโโ routes/
โ โ โโโ auth.routes.ts
โ โ โโโ tasks.routes.ts
โ โ โโโ index.ts
โ โโโ schemas/
โ โ โโโ auth.schema.ts
โ โ โโโ task.schema.ts
โ โโโ services/
โ โ โโโ auth.service.ts
โ โ โโโ task.service.ts
โ โโโ types/
โ โ โโโ index.ts
โ โโโ utils/
โ โ โโโ jwt.ts
โ โ โโโ password.ts
โ โโโ app.ts
โโโ prisma/
โ โโโ schema.prisma
โโโ .env
โโโ package.json
โโโ tsconfig.json
Database Schema
Define your database schema with Prisma:
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(uuid())
email String @unique
password String
name String
role Role @default(USER)
tasks Task[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Task {
id String @id @default(uuid())
title String
description String?
status TaskStatus @default(TODO)
priority Priority @default(MEDIUM)
dueDate DateTime?
userId String
user User @relation(fields: [userId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId])
@@index([status])
}
enum Role {
USER
ADMIN
}
enum TaskStatus {
TODO
IN_PROGRESS
COMPLETED
ARCHIVED
}
enum Priority {
LOW
MEDIUM
HIGH
URGENT
}
Run migrations:
npx prisma migrate dev --name init
npx prisma generate
Core Application Setup
Main Application File
// src/app.ts
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import morgan from 'morgan';
import 'express-async-errors';
import routes from './routes';
import { errorHandler } from './middleware/error.middleware';
const app = express();
// Security middleware
app.use(helmet());
app.use(cors({
origin: process.env.CORS_ORIGIN || '*',
credentials: true,
}));
// Request parsing
app.use(express.json({ limit: '10kb' }));
app.use(express.urlencoded({ extended: true }));
// Logging
if (process.env.NODE_ENV !== 'test') {
app.use(morgan('combined'));
}
// Health check
app.get('/health', (req, res) => {
res.json({ status: 'healthy', timestamp: new Date().toISOString() });
});
// API routes
app.use('/api/v1', routes);
// 404 handler
app.use((req, res) => {
res.status(404).json({
success: false,
message: `Route ${req.originalUrl} not found`,
});
});
// Global error handler
app.use(errorHandler);
export default app;
Server Entry Point
// src/server.ts
import app from './app';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
const PORT = process.env.PORT || 3000;
async function main() {
try {
// Test database connection
await prisma.$connect();
console.log('โ
Database connected');
app.listen(PORT, () => {
console.log(`๐ Server running on port ${PORT}`);
console.log(`๐ API Docs: http://localhost:${PORT}/api/v1`);
});
} catch (error) {
console.error('โ Failed to start server:', error);
process.exit(1);
}
}
// Graceful shutdown
process.on('SIGINT', async () => {
await prisma.$disconnect();
process.exit(0);
});
main();
Authentication System
Password Utilities
// src/utils/password.ts
import bcrypt from 'bcryptjs';
const SALT_ROUNDS = 12;
export async function hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, SALT_ROUNDS);
}
export async function comparePassword(
password: string,
hashedPassword: string
): Promise<boolean> {
return bcrypt.compare(password, hashedPassword);
}
JWT Utilities
// src/utils/jwt.ts
import jwt from 'jsonwebtoken';
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '7d';
interface TokenPayload {
userId: string;
email: string;
role: string;
}
export function generateToken(payload: TokenPayload): string {
return jwt.sign(payload, JWT_SECRET, {
expiresIn: JWT_EXPIRES_IN,
});
}
export function verifyToken(token: string): TokenPayload {
return jwt.verify(token, JWT_SECRET) as TokenPayload;
}
Auth Validation Schema
// src/schemas/auth.schema.ts
import { z } from 'zod';
export const registerSchema = z.object({
body: z.object({
email: z.string().email('Invalid email format'),
password: z
.string()
.min(8, 'Password must be at least 8 characters')
.regex(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
'Password must contain uppercase, lowercase, and number'
),
name: z.string().min(2, 'Name must be at least 2 characters'),
}),
});
export const loginSchema = z.object({
body: z.object({
email: z.string().email('Invalid email format'),
password: z.string().min(1, 'Password is required'),
}),
});
export type RegisterInput = z.infer<typeof registerSchema>['body'];
export type LoginInput = z.infer<typeof loginSchema>['body'];
Auth Controller
// src/controllers/auth.controller.ts
import { Request, Response } from 'express';
import { PrismaClient } from '@prisma/client';
import { hashPassword, comparePassword } from '../utils/password';
import { generateToken } from '../utils/jwt';
import { RegisterInput, LoginInput } from '../schemas/auth.schema';
const prisma = new PrismaClient();
export async function register(req: Request, res: Response) {
const { email, password, name }: RegisterInput = req.body;
// Check if user exists
const existingUser = await prisma.user.findUnique({
where: { email },
});
if (existingUser) {
return res.status(400).json({
success: false,
message: 'Email already registered',
});
}
// Hash password and create user
const hashedPassword = await hashPassword(password);
const user = await prisma.user.create({
data: {
email,
password: hashedPassword,
name,
},
select: {
id: true,
email: true,
name: true,
role: true,
createdAt: true,
},
});
// Generate token
const token = generateToken({
userId: user.id,
email: user.email,
role: user.role,
});
res.status(201).json({
success: true,
message: 'User registered successfully',
data: {
user,
token,
},
});
}
export async function login(req: Request, res: Response) {
const { email, password }: LoginInput = req.body;
// Find user
const user = await prisma.user.findUnique({
where: { email },
});
if (!user) {
return res.status(401).json({
success: false,
message: 'Invalid email or password',
});
}
// Verify password
const isValidPassword = await comparePassword(password, user.password);
if (!isValidPassword) {
return res.status(401).json({
success: false,
message: 'Invalid email or password',
});
}
// Generate token
const token = generateToken({
userId: user.id,
email: user.email,
role: user.role,
});
res.json({
success: true,
message: 'Login successful',
data: {
user: {
id: user.id,
email: user.email,
name: user.name,
role: user.role,
},
token,
},
});
}
export async function getProfile(req: Request, res: Response) {
const userId = req.user?.userId;
const user = await prisma.user.findUnique({
where: { id: userId },
select: {
id: true,
email: true,
name: true,
role: true,
createdAt: true,
_count: {
select: { tasks: true },
},
},
});
res.json({
success: true,
data: user,
});
}
Task CRUD Operations
Task Schema
// src/schemas/task.schema.ts
import { z } from 'zod';
export const createTaskSchema = z.object({
body: z.object({
title: z.string().min(1, 'Title is required').max(200),
description: z.string().max(2000).optional(),
priority: z.enum(['LOW', 'MEDIUM', 'HIGH', 'URGENT']).optional(),
dueDate: z.string().datetime().optional(),
}),
});
export const updateTaskSchema = z.object({
params: z.object({
id: z.string().uuid('Invalid task ID'),
}),
body: z.object({
title: z.string().min(1).max(200).optional(),
description: z.string().max(2000).optional(),
status: z.enum(['TODO', 'IN_PROGRESS', 'COMPLETED', 'ARCHIVED']).optional(),
priority: z.enum(['LOW', 'MEDIUM', 'HIGH', 'URGENT']).optional(),
dueDate: z.string().datetime().nullable().optional(),
}),
});
export const taskQuerySchema = z.object({
query: z.object({
status: z.enum(['TODO', 'IN_PROGRESS', 'COMPLETED', 'ARCHIVED']).optional(),
priority: z.enum(['LOW', 'MEDIUM', 'HIGH', 'URGENT']).optional(),
page: z.string().regex(/^\d+$/).optional().default('1'),
limit: z.string().regex(/^\d+$/).optional().default('10'),
sortBy: z.enum(['createdAt', 'dueDate', 'priority']).optional().default('createdAt'),
order: z.enum(['asc', 'desc']).optional().default('desc'),
}),
});
Task Controller
// src/controllers/tasks.controller.ts
import { Request, Response } from 'express';
import { PrismaClient, Prisma } from '@prisma/client';
const prisma = new PrismaClient();
// GET /api/v1/tasks
export async function getTasks(req: Request, res: Response) {
const userId = req.user!.userId;
const { status, priority, page, limit, sortBy, order } = req.query;
const pageNum = parseInt(page as string) || 1;
const limitNum = Math.min(parseInt(limit as string) || 10, 100);
const skip = (pageNum - 1) * limitNum;
// Build where clause
const where: Prisma.TaskWhereInput = {
userId,
...(status && { status: status as any }),
...(priority && { priority: priority as any }),
};
// Execute query with count
const [tasks, total] = await Promise.all([
prisma.task.findMany({
where,
skip,
take: limitNum,
orderBy: { [sortBy as string]: order },
select: {
id: true,
title: true,
description: true,
status: true,
priority: true,
dueDate: true,
createdAt: true,
updatedAt: true,
},
}),
prisma.task.count({ where }),
]);
res.json({
success: true,
data: tasks,
pagination: {
page: pageNum,
limit: limitNum,
total,
pages: Math.ceil(total / limitNum),
},
});
}
// GET /api/v1/tasks/:id
export async function getTask(req: Request, res: Response) {
const { id } = req.params;
const userId = req.user!.userId;
const task = await prisma.task.findFirst({
where: { id, userId },
});
if (!task) {
return res.status(404).json({
success: false,
message: 'Task not found',
});
}
res.json({
success: true,
data: task,
});
}
// POST /api/v1/tasks
export async function createTask(req: Request, res: Response) {
const userId = req.user!.userId;
const { title, description, priority, dueDate } = req.body;
const task = await prisma.task.create({
data: {
title,
description,
priority,
dueDate: dueDate ? new Date(dueDate) : null,
userId,
},
});
res.status(201).json({
success: true,
message: 'Task created successfully',
data: task,
});
}
// PUT /api/v1/tasks/:id
export async function updateTask(req: Request, res: Response) {
const { id } = req.params;
const userId = req.user!.userId;
const updates = req.body;
// Check ownership
const existing = await prisma.task.findFirst({
where: { id, userId },
});
if (!existing) {
return res.status(404).json({
success: false,
message: 'Task not found',
});
}
// Handle dueDate
if (updates.dueDate !== undefined) {
updates.dueDate = updates.dueDate ? new Date(updates.dueDate) : null;
}
const task = await prisma.task.update({
where: { id },
data: updates,
});
res.json({
success: true,
message: 'Task updated successfully',
data: task,
});
}
// DELETE /api/v1/tasks/:id
export async function deleteTask(req: Request, res: Response) {
const { id } = req.params;
const userId = req.user!.userId;
// Check ownership
const existing = await prisma.task.findFirst({
where: { id, userId },
});
if (!existing) {
return res.status(404).json({
success: false,
message: 'Task not found',
});
}
await prisma.task.delete({
where: { id },
});
res.json({
success: true,
message: 'Task deleted successfully',
});
}
Middleware
Authentication Middleware
// src/middleware/auth.middleware.ts
import { Request, Response, NextFunction } from 'express';
import { verifyToken } from '../utils/jwt';
declare global {
namespace Express {
interface Request {
user?: {
userId: string;
email: string;
role: string;
};
}
}
}
export function authenticate(req: Request, res: Response, next: NextFunction) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({
success: false,
message: 'Authentication required',
});
}
const token = authHeader.split(' ')[1];
try {
const payload = verifyToken(token);
req.user = payload;
next();
} catch (error) {
return res.status(401).json({
success: false,
message: 'Invalid or expired token',
});
}
}
export function authorize(...roles: string[]) {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.user) {
return res.status(401).json({
success: false,
message: 'Authentication required',
});
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({
success: false,
message: 'Insufficient permissions',
});
}
next();
};
}
Validation Middleware
// src/middleware/validate.middleware.ts
import { Request, Response, NextFunction } from 'express';
import { AnyZodObject, ZodError } from 'zod';
export function validate(schema: AnyZodObject) {
return async (req: Request, res: Response, next: NextFunction) => {
try {
await schema.parseAsync({
body: req.body,
query: req.query,
params: req.params,
});
next();
} catch (error) {
if (error instanceof ZodError) {
return res.status(400).json({
success: false,
message: 'Validation failed',
errors: error.errors.map(err => ({
field: err.path.join('.'),
message: err.message,
})),
});
}
next(error);
}
};
}
Error Handler Middleware
// src/middleware/error.middleware.ts
import { Request, Response, NextFunction } from 'express';
import { Prisma } from '@prisma/client';
export function errorHandler(
err: Error,
req: Request,
res: Response,
next: NextFunction
) {
console.error('Error:', err);
// Prisma errors
if (err instanceof Prisma.PrismaClientKnownRequestError) {
if (err.code === 'P2002') {
return res.status(409).json({
success: false,
message: 'Resource already exists',
});
}
if (err.code === 'P2025') {
return res.status(404).json({
success: false,
message: 'Resource not found',
});
}
}
// Default error response
res.status(500).json({
success: false,
message: process.env.NODE_ENV === 'production'
? 'Internal server error'
: err.message,
});
}
Routes
// src/routes/index.ts
import { Router } from 'express';
import authRoutes from './auth.routes';
import taskRoutes from './tasks.routes';
const router = Router();
router.use('/auth', authRoutes);
router.use('/tasks', taskRoutes);
export default router;
// src/routes/auth.routes.ts
import { Router } from 'express';
import { register, login, getProfile } from '../controllers/auth.controller';
import { validate } from '../middleware/validate.middleware';
import { authenticate } from '../middleware/auth.middleware';
import { registerSchema, loginSchema } from '../schemas/auth.schema';
const router = Router();
router.post('/register', validate(registerSchema), register);
router.post('/login', validate(loginSchema), login);
router.get('/profile', authenticate, getProfile);
export default router;
// src/routes/tasks.routes.ts
import { Router } from 'express';
import {
getTasks,
getTask,
createTask,
updateTask,
deleteTask,
} from '../controllers/tasks.controller';
import { validate } from '../middleware/validate.middleware';
import { authenticate } from '../middleware/auth.middleware';
import {
createTaskSchema,
updateTaskSchema,
taskQuerySchema,
} from '../schemas/task.schema';
const router = Router();
// All routes require authentication
router.use(authenticate);
router.get('/', validate(taskQuerySchema), getTasks);
router.get('/:id', getTask);
router.post('/', validate(createTaskSchema), createTask);
router.put('/:id', validate(updateTaskSchema), updateTask);
router.delete('/:id', deleteTask);
export default router;
API Response Format
Success Response
{
"success": true,
"message": "Task created successfully",
"data": {
"id": "uuid-here",
"title": "Complete API documentation",
"status": "TODO"
}
}
Error Response
{
"success": false,
"message": "Validation failed",
"errors": [
{
"field": "body.email",
"message": "Invalid email format"
}
]
}
Paginated Response
{
"success": true,
"data": [...],
"pagination": {
"page": 1,
"limit": 10,
"total": 45,
"pages": 5
}
}
Key Takeaways
- Use TypeScript for type safety and better developer experience
- Validate all inputs with Zod before processing
- Implement proper authentication with JWT tokens
- Use layered architecture: Routes โ Controllers โ Services
- Handle errors gracefully with a global error handler
- Always return consistent response formats
- Use Prisma for type-safe database operations
- Implement pagination for list endpoints
- Secure your API with helmet, cors, and rate limiting
- Write tests for critical endpoints
Next Steps
This guide covered the fundamentals of building a production-ready REST API with Node.js. To take it further, consider:
- Rate Limiting - Prevent abuse with express-rate-limit
- Caching - Add Redis for response caching
- API Documentation - Generate docs with Swagger/OpenAPI
- Testing - Add Jest and Supertest for automated testing
- CI/CD - Set up GitHub Actions for deployment
- Monitoring - Add logging and APM tools
Need help building your API? Contact us for a free consultation.
Last updated: January 2025

