Core Chunk Logo
Core Chunk
ServicesAboutWorkBlogCareersPricingContact
Get a Quote
Building a REST API with Node.js: Complete Guide for 2025
Back to Blog
Development

Building a REST API with Node.js: Complete Guide for 2025

D
dd
ยท
December 25, 2025
ยท
16 min read

Step-by-step tutorial for building production-ready REST APIs with Node.js, Express, TypeScript, and Prisma. Includes authentication, validation, and error handling.

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?

๐Ÿ“ Definition
REST (Representational State Transfer) is an architectural style for designing networked applications. A REST API uses HTTP requests to perform CRUD operations (Create, Read, Update, Delete) on resources.

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

๐Ÿ’ก REST Best Practice
Use plural nouns for resources (/tasks, not /task) and HTTP verbs for actions (GET, POST, PUT, DELETE).

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

๐Ÿ’ก Consistency Matters
Always use a consistent response format across all endpoints. This makes your API predictable and easier to consume.

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

๐ŸŽฏ 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:

  1. Rate Limiting - Prevent abuse with express-rate-limit
  2. Caching - Add Redis for response caching
  3. API Documentation - Generate docs with Swagger/OpenAPI
  4. Testing - Add Jest and Supertest for automated testing
  5. CI/CD - Set up GitHub Actions for deployment
  6. Monitoring - Add logging and APM tools

Need help building your API? Contact us for a free consultation.

Last updated: January 2025

nextjs
D
dd

devd

Continue Reading

More articles you might enjoy

Building Scalable React Applications: Complete Architecture Guide
Development

Building Scalable React Applications: Complete Architecture Guide

Learn proven patterns and practices for building React applications that scale. Covers project structure, state management, performance optimization, and testing strategies.

14 min
Read
Web Development Cost in India 2025: Complete Pricing Guide
Development

Web Development Cost in India 2025: Complete Pricing Guide

Comprehensive guide to web development pricing in India for 2025. From static websites to complex web applications, understand costs, factors, and how to choose the right development partner.

12 min
Read

Let's build something great

Subscribe for tips, tutorials, and updates on web development, design, and marketing.

Cities We Serve in Andhra Pradesh

VijayawadaGunturTenaliMangalagiriPedakakaniTadepalliVisakhapatnamGajuwakaAnakapalliVizianagaramBobbiliSrikakulamKakinadaRajahmundryAmalapuramEluruBhimavaramBapatlaChiralaNarasaraopetOngoleNelloreGudurTirupatiSrikalahastiChittoorMadanapalleRajampetKadapaProddaturKurnoolAdoniNandyalAnantapurHindupurMachilipatnam
Core Chunk Logo
Core Chunk

Full-service digital agency helping businesses grow through innovative web development, design, and marketing solutions.

support@corechunk.com
+91-8465864543
Hyderabad, Telangana, IN

Services

  • Web Dev
  • Mobile Apps
  • APIs
  • Integrations
  • UX Design
  • Branding
  • SEO
  • Email
  • SMS
  • Analytics
  • Hosting
  • Support
  • LLM Solutions
  • RAG Systems
  • AI Automation
  • Chatbots
  • Vision AI
  • Predictive AI
  • AI Consulting
  • Enterprise GenAI
  • RAG Solutions
  • App Modernization
  • Fintech Solutions
  • HealthTech
  • EdTech Solutions

Company

  • About Us
  • Our Work
  • Blog
  • CareersHiring!
  • Contact

Legal

  • Privacy Policy
  • Terms of Service
  • Cookie Policy
ยฉ 2026 Core Chunk. All rights reserved.
Made with in Hyderabad