Complete Node.js REST API Tutorial

A comprehensive guide to building RESTful APIs with Node.js - from theory to practice with simple examples.

REST Principles CRUD Best Practices

Table of Contents

  1. What is a REST API?
  2. REST API Principles
  3. Setting Up Your First API
  4. CRUD Operations Explained
  5. Working with Data
  6. Route Parameters & Query Strings
  7. Request & Response Handling
  8. Error Handling
  9. API Versioning
  10. Best Practices
  11. Quick Reference
  12. Interview Q&A + MCQ
  13. Contextual Learning Links

1. What is a REST API?

The Theory

REST API (Representational State Transfer Application Programming Interface) is a way for different software applications to communicate with each other over the internet.

Simple Analogy

Think of a REST API like a restaurant menu:

You (Client)                    Waiter (API)                    Kitchen (Server)
    │                                │                                 │
    │  "I want to SEE all pizzas"    │                                 │
    │  (GET /pizzas)                  │                                 │
    │ ───────────────────────────────>│                                 │
    │                                │  "Get all pizzas"               │
    │                                │ ───────────────────────────────>│
    │                                │                                 │
    │                                │  "Here are the pizzas"          │
    │                                │ <───────────────────────────────│
    │  "Here are the pizzas"         │                                 │
    │ <───────────────────────────────│                                 │

Key points:

  • Client: Your app, website, or mobile app
  • API: The messenger that follows rules
  • Server: Where the data lives
  • Resource: What you're requesting (pizzas, users, products)

Why Use REST APIs?

BenefitExplanationReal-world example
StandardizedWorks the same way everywhereAny app can talk to Twitter's API
StatelessEach request is independentServer doesn't need to "remember" you
ScalableCan handle millions of requestsGoogle Maps API serves billions of requests
FlexibleReturns data in multiple formatsJSON, XML, HTML

REST API vs Regular Website

Regular WebsiteREST API
Returns HTML pagesReturns data (usually JSON)
Designed for humansDesigned for applications
Includes stylingJust raw data
Browser renders itApp processes it
// Regular website response (HTML)
// <html><body><h1>John Doe</h1><p>Age: 30</p></body></html>

// REST API response (JSON)
// {"name":"John Doe","age":30}

2. REST API Principles

The Theory

REST APIs follow 6 main principles (constraints) that make them RESTful:

1. Client-Server Separation

Theory: The client (frontend) and server (backend) are separate and can evolve independently.

Client (React/Vue) ←→ API ←→ Server (Node.js)
     ↓                      ↓
  UI changes            Database changes
  (won't break API)     (won't break UI)

2. Statelessness

Theory: The server doesn't remember anything between requests. Each request must contain all necessary information.

// ❌ BAD: Stateful API (server remembers)
// Request 1: POST /login {username: "john"}
// Server: "Here's your session ID: 123"
// Request 2: GET /profile (server remembers you from session 123)

// ✅ GOOD: Stateless API
// Request 1: POST /login {username: "john"}
// Response: {token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"}
// Request 2: GET /profile Headers: {Authorization: "Bearer token"}
// (Client sends token with EVERY request)

3. Cacheability

Theory: Responses should indicate whether they can be cached to improve performance.

// Tell client to cache this response for 1 hour
res.setHeader('Cache-Control', 'public, max-age=3600');

4. Uniform Interface

Theory: Use standard HTTP methods and status codes consistently.

ActionHTTP MethodURL ExampleWhat it does
CreatePOST/usersAdd a new user
ReadGET/users/1Get user #1
Update (full)PUT/users/1Replace user #1
Update (partial)PATCH/users/1Update user's email
DeleteDELETE/users/1Remove user #1
ListGET/usersGet all users

5. Layered System

Theory: The API can sit behind proxies, load balancers, or other layers without the client knowing.

Client → Load Balancer → API Server → Database
         (Client doesn't know about the load balancer)

6. Code on Demand (Optional)

Theory: Server can send executable code to the client (rarely used).

Simple Example: RESTful Endpoints

const http = require('http');

// The same URL with different HTTP methods does different things
const server = http.createServer((req, res) => {
    const url = req.url;
    
    // GET /users - List all users (READ)
    if (req.method === 'GET' && url === '/users') {
        res.end('Will return list of users');
    }
    
    // POST /users - Create a new user (CREATE)
    else if (req.method === 'POST' && url === '/users') {
        res.end('Will create a new user');
    }
    
    // GET /users/1 - Get specific user (READ one)
    else if (req.method === 'GET' && url.match(/^\/users\/\d+$/)) {
        res.end('Will return user #1');
    }
    
    // PUT /users/1 - Update entire user (UPDATE)
    else if (req.method === 'PUT' && url.match(/^\/users\/\d+$/)) {
        res.end('Will update user #1');
    }
    
    // DELETE /users/1 - Delete user (DELETE)
    else if (req.method === 'DELETE' && url.match(/^\/users\/\d+$/)) {
        res.end('Will delete user #1');
    }
});

server.listen(3000);

3. Setting Up Your First API

Theory: Project Structure

A basic REST API project structure:

my-api/
├── server.js          # Main server file
├── data/              # Data storage (for learning)
│   └── users.json
├── routes/            # API route handlers
│   └── users.js
└── package.json       # Project dependencies

Simple Example: Your First REST API

// server.js - Complete working REST API
const http = require('http');

// In-memory database (data storage)
let users = [
    { id: 1, name: 'Alice', email: 'alice@example.com' },
    { id: 2, name: 'Bob', email: 'bob@example.com' }
];

// Helper function to send JSON responses
function sendJSON(res, statusCode, data) {
    res.writeHead(statusCode, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify(data));
}

// Helper function to parse request body
function parseBody(req) {
    return new Promise((resolve, reject) => {
        let body = '';
        req.on('data', chunk => body += chunk);
        req.on('end', () => {
            try {
                resolve(body ? JSON.parse(body) : {});
            } catch (error) {
                reject(error);
            }
        });
        req.on('error', reject);
    });
}

const server = http.createServer(async (req, res) => {
    const { method, url } = req;
    
    // GET /users - Get all users
    if (method === 'GET' && url === '/users') {
        sendJSON(res, 200, users);
    }
    
    // GET /users/1 - Get one user
    else if (method === 'GET' && url.match(/^\/users\/\d+$/)) {
        const id = parseInt(url.split('/')[2]);
        const user = users.find(u => u.id === id);
        
        if (user) {
            sendJSON(res, 200, user);
        } else {
            sendJSON(res, 404, { error: 'User not found' });
        }
    }
    
    // POST /users - Create new user
    else if (method === 'POST' && url === '/users') {
        try {
            const newUser = await parseBody(req);
            newUser.id = users.length + 1;
            users.push(newUser);
            sendJSON(res, 201, newUser);
        } catch (error) {
            sendJSON(res, 400, { error: 'Invalid JSON' });
        }
    }
    
    // PUT /users/1 - Update user
    else if (method === 'PUT' && url.match(/^\/users\/\d+$/)) {
        const id = parseInt(url.split('/')[2]);
        const index = users.findIndex(u => u.id === id);
        
        if (index === -1) {
            sendJSON(res, 404, { error: 'User not found' });
            return;
        }
        
        try {
            const updates = await parseBody(req);
            users[index] = { ...users[index], ...updates };
            sendJSON(res, 200, users[index]);
        } catch (error) {
            sendJSON(res, 400, { error: 'Invalid JSON' });
        }
    }
    
    // DELETE /users/1 - Delete user
    else if (method === 'DELETE' && url.match(/^\/users\/\d+$/)) {
        const id = parseInt(url.split('/')[2]);
        const index = users.findIndex(u => u.id === id);
        
        if (index === -1) {
            sendJSON(res, 404, { error: 'User not found' });
            return;
        }
        
        users.splice(index, 1);
        sendJSON(res, 200, { message: 'User deleted successfully' });
    }
    
    // 404 - Route not found
    else {
        sendJSON(res, 404, { error: 'Route not found' });
    }
});

server.listen(3000, () => {
    console.log('REST API running on http://localhost:3000');
    console.log('\nTry these commands:');
    console.log('  GET    /users              - List all users');
    console.log('  GET    /users/1            - Get user #1');
    console.log('  POST   /users              - Create a user');
    console.log('  PUT    /users/1            - Update user #1');
    console.log('  DELETE /users/1            - Delete user #1');
});

// Test with curl (in another terminal):
// curl http://localhost:3000/users
// curl -X POST -H "Content-Type: application/json" -d '{"name":"Charlie","email":"charlie@example.com"}' http://localhost:3000/users
// curl -X PUT -H "Content-Type: application/json" -d '{"name":"Charles"}' http://localhost:3000/users/3
// curl -X DELETE http://localhost:3000/users/3

Testing Your API

You can test your API using:

  • curl (command line)
  • Postman (desktop app)
  • Browser (for GET requests)
  • Your own frontend app
# Test GET request (in terminal)
curl http://localhost:3000/users

# Test POST request
curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"name":"Charlie","email":"charlie@example.com"}' \
  http://localhost:3000/users

# Test PUT request
curl -X PUT \
  -H "Content-Type: application/json" \
  -d '{"name":"Charles"}' \
  http://localhost:3000/users/3

# Test DELETE request
curl -X DELETE http://localhost:3000/users/3

4. CRUD Operations Explained

The Theory

CRUD stands for Create, Read, Update, Delete - the four basic operations for any data storage system.

OperationHTTP MethodEndpointDescription
CreatePOST/usersAdd new user
ReadGET/usersGet all users
Read oneGET/users/1Get specific user
UpdatePUT/PATCH/users/1Modify user
DeleteDELETE/users/1Remove user

Simple Example: Complete CRUD API

const http = require('http');

// Our data store
let products = [
    { id: 1, name: 'Laptop', price: 999, inStock: true },
    { id: 2, name: 'Mouse', price: 25, inStock: true }
];

const server = http.createServer(async (req, res) => {
    const { method, url } = req;
    
    // Helper functions
    const sendJSON = (status, data) => {
        res.writeHead(status, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify(data));
    };
    
    const getBody = () => {
        return new Promise((resolve, reject) => {
            let body = '';
            req.on('data', chunk => body += chunk);
            req.on('end', () => {
                try {
                    resolve(body ? JSON.parse(body) : {});
                } catch (e) {
                    reject(e);
                }
            });
            req.on('error', reject);
        });
    };
    
    // CREATE - POST /products
    if (method === 'POST' && url === '/products') {
        try {
            const newProduct = await getBody();
            newProduct.id = products.length + 1;
            products.push(newProduct);
            sendJSON(201, newProduct);
        } catch (error) {
            sendJSON(400, { error: 'Invalid product data' });
        }
    }
    
    // READ ALL - GET /products
    else if (method === 'GET' && url === '/products') {
        sendJSON(200, products);
    }
    
    // READ ONE - GET /products/1
    else if (method === 'GET' && url.match(/^\/products\/\d+$/)) {
        const id = parseInt(url.split('/')[2]);
        const product = products.find(p => p.id === id);
        
        if (product) {
            sendJSON(200, product);
        } else {
            sendJSON(404, { error: 'Product not found' });
        }
    }
    
    // UPDATE - PUT /products/1
    else if (method === 'PUT' && url.match(/^\/products\/\d+$/)) {
        const id = parseInt(url.split('/')[2]);
        const index = products.findIndex(p => p.id === id);
        
        if (index === -1) {
            sendJSON(404, { error: 'Product not found' });
            return;
        }
        
        try {
            const updates = await getBody();
            products[index] = { ...products[index], ...updates };
            sendJSON(200, products[index]);
        } catch (error) {
            sendJSON(400, { error: 'Invalid update data' });
        }
    }
    
    // DELETE - DELETE /products/1
    else if (method === 'DELETE' && url.match(/^\/products\/\d+$/)) {
        const id = parseInt(url.split('/')[2]);
        const index = products.findIndex(p => p.id === id);
        
        if (index === -1) {
            sendJSON(404, { error: 'Product not found' });
            return;
        }
        
        products.splice(index, 1);
        sendJSON(200, { message: 'Product deleted' });
    }
    
    else {
        sendJSON(404, { error: 'Endpoint not found' });
    }
});

server.listen(3000, () => {
    console.log('Product API running on port 3000');
});

// Test commands:
// CREATE: curl -X POST -H "Content-Type: application/json" -d '{"name":"Keyboard","price":50,"inStock":true}' http://localhost:3000/products
// READ:   curl http://localhost:3000/products
// READ 1: curl http://localhost:3000/products/1
// UPDATE: curl -X PUT -H "Content-Type: application/json" -d '{"price":45}' http://localhost:3000/products/3
// DELETE: curl -X DELETE http://localhost:3000/products/3

PUT vs PATCH (Understanding the Difference)

PUTPATCH
Replace entire resourceUpdate specific fields
Send all fieldsSend only changed fields
If field missing, it becomes nullMissing fields stay the same
// Example: Current user = { id: 1, name: "John", age: 30, email: "john@example.com" }

// PUT request (send everything)
PUT /users/1
{
    "name": "Johnny",     // Only sending name
    // age and email would be deleted!
}
// Result: { id: 1, name: "Johnny" } — age and email are GONE!

// PATCH request (send only changes)
PATCH /users/1
{
    "name": "Johnny"      // Only changing name
}
// Result: { id: 1, name: "Johnny", age: 30, email: "john@example.com" }
// Other fields remain unchanged!

5. Working with Data

Theory: Data Storage Options

Storage TypeBest forExample
In-memoryLearning/prototypingArrays, variables
File systemSmall apps, configJSON files
SQL databaseStructured dataPostgreSQL, MySQL
NoSQL databaseFlexible dataMongoDB

Simple Example: File-based Storage

const http = require('http');
const fs = require('fs').promises;
const path = require('path');

const DATA_FILE = path.join(__dirname, 'data.json');

// Initialize data file if it doesn't exist
async function initDataFile() {
    try {
        await fs.access(DATA_FILE);
    } catch {
        await fs.writeFile(DATA_FILE, JSON.stringify([]));
    }
}

// Read data from file
async function readData() {
    const data = await fs.readFile(DATA_FILE, 'utf8');
    return JSON.parse(data);
}

// Write data to file
async function writeData(data) {
    await fs.writeFile(DATA_FILE, JSON.stringify(data, null, 2));
}

const server = http.createServer(async (req, res) => {
    const { method, url } = req;
    
    const sendJSON = (status, data) => {
        res.writeHead(status, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify(data));
    };
    
    const getBody = () => {
        return new Promise((resolve, reject) => {
            let body = '';
            req.on('data', chunk => body += chunk);
            req.on('end', () => {
                try {
                    resolve(body ? JSON.parse(body) : {});
                } catch (e) {
                    reject(e);
                }
            });
        });
    };
    
    try {
        // GET /tasks - Get all tasks
        if (method === 'GET' && url === '/tasks') {
            const tasks = await readData();
            sendJSON(200, tasks);
        }
        
        // POST /tasks - Create task
        else if (method === 'POST' && url === '/tasks') {
            const tasks = await readData();
            const newTask = await getBody();
            newTask.id = tasks.length + 1;
            newTask.createdAt = new Date().toISOString();
            tasks.push(newTask);
            await writeData(tasks);
            sendJSON(201, newTask);
        }
        
        // DELETE /tasks/1 - Delete task
        else if (method === 'DELETE' && url.match(/^\/tasks\/\d+$/)) {
            const id = parseInt(url.split('/')[2]);
            let tasks = await readData();
            const filtered = tasks.filter(t => t.id !== id);
            
            if (tasks.length === filtered.length) {
                sendJSON(404, { error: 'Task not found' });
                return;
            }
            
            await writeData(filtered);
            sendJSON(200, { message: 'Task deleted' });
        }
        
        else {
            sendJSON(404, { error: 'Not found' });
        }
    } catch (error) {
        console.error(error);
        sendJSON(500, { error: 'Server error' });
    }
});

initDataFile().then(() => {
    server.listen(3000, () => {
        console.log('Task API running on port 3000');
        console.log('Data saved to:', DATA_FILE);
    });
});

// Test commands:
// curl -X POST -H "Content-Type: application/json" -d '{"title":"Learn Node.js","completed":false}' http://localhost:3000/tasks
// curl http://localhost:3000/tasks
// curl -X DELETE http://localhost:3000/tasks/1

6. Route Parameters & Query Strings

The Theory

Route Parameters are variables in the URL path:

GET /users/123
           ^^^
      Route parameter (user ID)

Query Strings are optional parameters after ?:

GET /users?page=2&limit=10&sort=name
         ^^^^^^^^^^^^^^^^^^^^^^^^^^
              Query parameters

Simple Example: Using Route Params

const http = require('http');

let orders = [
    { id: 1, product: 'Laptop', quantity: 1, status: 'pending' },
    { id: 2, product: 'Mouse', quantity: 2, status: 'shipped' },
    { id: 3, product: 'Keyboard', quantity: 1, status: 'delivered' }
];

const server = http.createServer((req, res) => {
    const { method, url } = req;
    
    const sendJSON = (status, data) => {
        res.writeHead(status, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify(data));
    };
    
    // Route parameters example
    // GET /orders/2 - Get order #2
    if (method === 'GET' && url.match(/^\/orders\/\d+$/)) {
        const id = parseInt(url.split('/')[2]);  // Extract ID from URL
        const order = orders.find(o => o.id === id);
        
        if (order) {
            sendJSON(200, order);
        } else {
            sendJSON(404, { error: `Order ${id} not found` });
        }
    }
    
    // DELETE /orders/2 - Delete order #2
    else if (method === 'DELETE' && url.match(/^\/orders\/\d+$/)) {
        const id = parseInt(url.split('/')[2]);
        const index = orders.findIndex(o => o.id === id);
        
        if (index !== -1) {
            orders.splice(index, 1);
            sendJSON(200, { message: `Order ${id} deleted` });
        } else {
            sendJSON(404, { error: `Order ${id} not found` });
        }
    }
    
    // Query parameters example
    // GET /orders?status=pending
    else if (method === 'GET' && url.match(/^\/orders(\?.*)?$/)) {
        const urlParts = url.split('?');
        const queryParams = urlParts[1] ? new URLSearchParams(urlParts[1]) : null;
        
        let filteredOrders = [...orders];
        
        // Filter by status if provided
        if (queryParams && queryParams.has('status')) {
            const status = queryParams.get('status');
            filteredOrders = filteredOrders.filter(o => o.status === status);
        }
        
        // Add pagination
        const page = queryParams ? parseInt(queryParams.get('page')) || 1 : 1;
        const limit = queryParams ? parseInt(queryParams.get('limit')) || 10 : 10;
        const start = (page - 1) * limit;
        const paginatedOrders = filteredOrders.slice(start, start + limit);
        
        sendJSON(200, {
            data: paginatedOrders,
            pagination: {
                page,
                limit,
                total: filteredOrders.length,
                pages: Math.ceil(filteredOrders.length / limit)
            }
        });
    }
    
    else {
        sendJSON(404, { error: 'Route not found' });
    }
});

server.listen(3000, () => {
    console.log('Orders API running on port 3000');
    console.log('\nTry these URLs:');
    console.log('  /orders                - All orders');
    console.log('  /orders?status=pending - Filter by status');
    console.log('  /orders?page=1&limit=1 - Pagination');
    console.log('  /orders/1              - Specific order');
});

// Test commands:
// curl http://localhost:3000/orders
// curl http://localhost:3000/orders?status=pending
// curl http://localhost:3000/orders?page=1&limit=2
// curl http://localhost:3000/orders/1
// curl -X DELETE http://localhost:3000/orders/1

Multiple Route Parameters

// Example with multiple route parameters
// GET /users/123/posts/456
//        ^^^        ^^^
//     user ID     post ID

if (method === 'GET' && url.match(/^\/users\/\d+\/posts\/\d+$/)) {
    const parts = url.split('/');
    const userId = parseInt(parts[2]);   // After 'users'
    const postId = parseInt(parts[4]);   // After 'posts'
    
    sendJSON(200, { userId, postId, message: `User ${userId}'s post ${postId}` });
}

7. Request & Response Handling

Theory: What's in a Request/Response?

HTTP Request contains:

  • Method (GET, POST, etc.)
  • URL (path + query string)
  • Headers (metadata)
  • Body (data for POST/PUT)

HTTP Response contains:

  • Status code (200, 404, etc.)
  • Headers (content type, etc.)
  • Body (JSON data)

Simple Example: Complete Request/Response Handling

const http = require('http');

const server = http.createServer(async (req, res) => {
    console.log('\n=== NEW REQUEST ===');
    
    // 1. REQUEST INFORMATION
    console.log('Method:', req.method);
    console.log('URL:', req.url);
    console.log('Headers:', JSON.stringify(req.headers, null, 2));
    
    // 2. READ REQUEST BODY (if any)
    let body = '';
    req.on('data', chunk => body += chunk);
    
    await new Promise(resolve => {
        req.on('end', () => {
            if (body) {
                console.log('Body:', body);
                try {
                    const jsonBody = JSON.parse(body);
                    console.log('Parsed Body:', jsonBody);
                } catch (e) {
                    console.log('Body is not JSON');
                }
            }
            resolve();
        });
    });
    
    // 3. PROCESS THE REQUEST
    let responseData = {};
    let statusCode = 200;
    
    if (req.url === '/') {
        responseData = { message: 'Welcome to the API' };
    } 
    else if (req.url === '/echo') {
        responseData = {
            message: 'Echoing your request',
            method: req.method,
            headers: req.headers,
            body: body || null
        };
    }
    else if (req.url === '/error') {
        statusCode = 500;
        responseData = { error: 'Something went wrong' };
    }
    else {
        statusCode = 404;
        responseData = { error: 'Endpoint not found' };
    }
    
    // 4. SEND RESPONSE
    console.log('Response Status:', statusCode);
    console.log('Response Body:', responseData);
    
    // Set response headers
    res.writeHead(statusCode, {
        'Content-Type': 'application/json',
        'X-Powered-By': 'Node.js API',
        'X-Request-ID': Math.random().toString(36).substring(7)
    });
    
    // Send response body
    res.end(JSON.stringify(responseData));
});

server.listen(3000, () => {
    console.log('Demo API running on http://localhost:3000');
    console.log('\nTry these requests:');
    console.log('  curl http://localhost:3000/');
    console.log('  curl http://localhost:3000/echo');
    console.log('  curl -X POST -H "Content-Type: application/json" -d \'{"test":"data"}\' http://localhost:3000/echo');
    console.log('  curl http://localhost:3000/error');
});

Response Headers Explained

// Common response headers
res.writeHead(200, {
    // Tell client what kind of data we're sending
    'Content-Type': 'application/json',
    
    // How long to cache (3600 seconds = 1 hour)
    'Cache-Control': 'public, max-age=3600',
    
    // Allow any website to access this API (CORS)
    'Access-Control-Allow-Origin': '*',
    
    // Tell client the API version
    'API-Version': '1.0.0',
    
    // Custom headers for debugging
    'X-Response-Time': Date.now() - startTime,
    'X-Request-ID': uuid
});

8. Error Handling

Theory: Types of API Errors

Error TypeStatus CodeWhen it happens
Bad Request400Client sends wrong data format
Unauthorized401No authentication provided
Forbidden403Authenticated but not allowed
Not Found404Resource doesn't exist
Conflict409Resource already exists
Validation Error422Data doesn't meet requirements
Server Error500Something broke on our end

Simple Example: Complete Error Handling

const http = require('http');

// Our data store
let users = [
    { id: 1, email: 'alice@example.com', password: 'secret123' }
];

// Validation functions
function validateUser(user) {
    const errors = [];
    
    if (!user.email) {
        errors.push('Email is required');
    } else if (!user.email.includes('@')) {
        errors.push('Invalid email format');
    }
    
    if (!user.password) {
        errors.push('Password is required');
    } else if (user.password.length < 6) {
        errors.push('Password must be at least 6 characters');
    }
    
    return errors;
}

const server = http.createServer(async (req, res) => {
    const { method, url } = req;
    
    const sendError = (status, message, details = null) => {
        const errorResponse = {
            error: message,
            statusCode: status,
            timestamp: new Date().toISOString(),
            path: url
        };
        
        if (details) errorResponse.details = details;
        
        res.writeHead(status, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify(errorResponse));
    };
    
    const sendSuccess = (status, data) => {
        res.writeHead(status, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify(data));
    };
    
    const getBody = () => {
        return new Promise((resolve, reject) => {
            let body = '';
            req.on('data', chunk => body += chunk);
            req.on('end', () => {
                try {
                    resolve(body ? JSON.parse(body) : {});
                } catch (error) {
                    reject(new Error('Invalid JSON format'));
                }
            });
            req.on('error', () => reject(new Error('Error reading request')));
        });
    };
    
    try {
        // POST /users - Create user with validation
        if (method === 'POST' && url === '/users') {
            let userData;
            
            try {
                userData = await getBody();
            } catch (error) {
                sendError(400, error.message);
                return;
            }
            
            // Validate data
            const validationErrors = validateUser(userData);
            if (validationErrors.length > 0) {
                sendError(422, 'Validation failed', validationErrors);
                return;
            }
            
            // Check if email already exists
            const existingUser = users.find(u => u.email === userData.email);
            if (existingUser) {
                sendError(409, 'Email already registered');
                return;
            }
            
            // Create user
            const newUser = {
                id: users.length + 1,
                email: userData.email,
                createdAt: new Date().toISOString()
            };
            users.push(newUser);
            
            sendSuccess(201, newUser);
        }
        
        // GET /users/:id - Get user with error handling
        else if (method === 'GET' && url.match(/^\/users\/\d+$/)) {
            const id = parseInt(url.split('/')[2]);
            
            if (isNaN(id)) {
                sendError(400, 'Invalid user ID format');
                return;
            }
            
            const user = users.find(u => u.id === id);
            
            if (!user) {
                sendError(404, `User with ID ${id} not found`);
                return;
            }
            
            sendSuccess(200, user);
        }
        
        // DELETE /users/:id - Delete user
        else if (method === 'DELETE' && url.match(/^\/users\/\d+$/)) {
            const id = parseInt(url.split('/')[2]);
            const index = users.findIndex(u => u.id === id);
            
            if (index === -1) {
                sendError(404, `User with ID ${id} not found`);
                return;
            }
            
            users.splice(index, 1);
            sendSuccess(200, { message: `User ${id} deleted successfully` });
        }
        
        else {
            sendError(404, `Cannot ${method} ${url}`);
        }
        
    } catch (error) {
        console.error('Unexpected error:', error);
        sendError(500, 'Internal server error');
    }
});

server.listen(3000, () => {
    console.log('Error handling demo running on port 3000');
    console.log('\nTest error scenarios:');
    console.log('  curl -X POST -H "Content-Type: application/json" -d \'{"email":"invalid"}\' http://localhost:3000/users');
    console.log('  curl http://localhost:3000/users/999');
    console.log('  curl -X POST -H "Content-Type: application/json" -d \'not json\' http://localhost:3000/users');
});

9. API Versioning

Theory: Why Version APIs?

APIs change over time. Versioning allows you to:

  • Add new features without breaking existing apps
  • Deprecate old features gradually
  • Maintain backward compatibility

Versioning Strategies

StrategyURL ExampleHeader Example
URL path/v1/users-
Query param/users?version=1-
Custom header/usersAPI-Version: 1
Content negotiation/usersAccept: application/vnd.api.v1+json

Simple Example: Versioned API

const http = require('http');

// Version 1 of our API
const apiV1 = {
    getUsers: () => {
        return [
            { id: 1, name: 'Alice', email: 'alice@example.com' }
        ];
    },
    
    createUser: (data) => {
        return {
            id: 2,
            name: data.name,
            email: data.email,
            created: new Date().toISOString()
        };
    }
};

// Version 2 of our API (new features)
const apiV2 = {
    getUsers: () => {
        return [
            { 
                id: 1, 
                name: 'Alice', 
                email: 'alice@example.com',
                profile: {
                    avatar: 'https://api.example.com/avatars/1.jpg',
                    joined: '2024-01-01'
                }
            }
        ];
    },
    
    createUser: (data) => {
        return {
            id: 2,
            name: data.name,
            email: data.email,
            profile: {
                avatar: data.avatar || null,
                joined: new Date().toISOString()
            },
            preferences: data.preferences || {}
        };
    }
};

const server = http.createServer(async (req, res) => {
    const { method, url } = req;
    
    const sendJSON = (status, data) => {
        res.writeHead(status, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify(data));
    };
    
    // Extract version from URL (e.g., /v1/users)
    const versionMatch = url.match(/^\/(v\d+)\//);
    
    if (!versionMatch) {
        sendJSON(400, { 
            error: 'API version required',
            message: 'Please specify version: /v1/users, /v2/users',
            availableVersions: ['v1', 'v2']
        });
        return;
    }
    
    const version = versionMatch[1];
    const api = version === 'v2' ? apiV2 : apiV1;
    const cleanUrl = url.replace(`/${version}`, '');
    
    // Route handling based on version
    if (method === 'GET' && cleanUrl === '/users') {
        const users = api.getUsers();
        sendJSON(200, {
            version,
            data: users,
            count: users.length
        });
    }
    
    else if (method === 'POST' && cleanUrl === '/users') {
        let body = '';
        req.on('data', chunk => body += chunk);
        req.on('end', () => {
            const userData = JSON.parse(body);
            const newUser = api.createUser(userData);
            sendJSON(201, {
                version,
                message: 'User created',
                user: newUser
            });
        });
    }
    
    else {
        sendJSON(404, { 
            error: 'Route not found',
            version,
            availableRoutes: ['GET /users', 'POST /users']
        });
    }
});

server.listen(3000, () => {
    console.log('Versioned API running on http://localhost:3000');
    console.log('\nTry different versions:');
    console.log('  curl http://localhost:3000/v1/users');
    console.log('  curl http://localhost:3000/v2/users');
    console.log('  curl -X POST -H "Content-Type: application/json" -d \'{"name":"Bob","email":"bob@example.com"}\' http://localhost:3000/v1/users');
    console.log('  curl -X POST -H "Content-Type: application/json" -d \'{"name":"Bob","email":"bob@example.com","preferences":{"theme":"dark"}}\' http://localhost:3000/v2/users');
});

Deprecation Warnings

// Adding deprecation headers
res.setHeader('Warning', '299 - "API version 1 is deprecated. Please upgrade to v2"');
res.setHeader('Deprecation', 'true');
res.setHeader('Sunset', 'Wed, 31 Dec 2025 23:59:59 GMT');

10. Best Practices

Theory: REST API Best Practices

  • Use nouns, not verbs in URLs
  • Use proper HTTP methods
  • Use proper status codes
  • Version your API
  • Validate input
  • Use consistent naming
  • Document your API
  • Handle errors gracefully

Quick Reference

REST API Cheatsheet

ActionMethodURLRequest BodyResponse
List allGET/itemsNone[{...}]
Get oneGET/items/1None{...}
CreatePOST/items{...}{id: 3,...}
ReplacePUT/items/1{...}{...}
UpdatePATCH/items/1{changes}{...}
DeleteDELETE/items/1None{message}

Common Status Codes

200 OK                    // Success
201 Created              // Resource created
204 No Content           // Success, nothing to return
304 Not Modified         // Cache hit
400 Bad Request          // Client error
401 Unauthorized         // Need login
403 Forbidden            // Not allowed
404 Not Found            // Doesn't exist
409 Conflict             // Already exists
422 Validation Error     // Invalid data
500 Server Error         // Our fault

Response Format Best Practice

// Success response
{
    "success": true,
    "data": {...},
    "message": "Optional message",
    "timestamp": "2024-01-01T00:00:00.000Z"
}

// Error response
{
    "success": false,
    "error": "Error message",
    "statusCode": 400,
    "timestamp": "2024-01-01T00:00:00.000Z"
}

Naming Conventions

✅ Good❌ Bad
/users/getUsers
/users/123/user/123
/user-posts/userPosts
/posts?status=active/posts/active

10 Interview Questions + 10 MCQs

Interview Pattern 10 Q&A
1What is the main purpose of a REST API?easy
Answer: To enable standardized communication between clients and servers using HTTP.
2Why is statelessness important in REST?easy
Answer: Each request is independent, which improves scalability and simplifies server design.
3Difference between PUT and PATCH?medium
Answer: PUT usually replaces a full resource; PATCH updates selected fields.
4When should an API return 201?easy
Answer: After successful resource creation, typically for POST create operations.
5How do query parameters help APIs?medium
Answer: They support filtering, sorting, pagination, and optional request behavior.
6What is API versioning and why use it?medium
Answer: It allows evolving APIs without breaking existing consumers.
7Why should response formats be consistent?medium
Answer: Consistency makes client integration easier and reduces parsing/handling errors.
8When should you use 404 vs 400?hard
Answer: Use 404 when resource/route doesn't exist; 400 for malformed client requests.
9What is a layered system in REST?hard
Answer: Clients interact with APIs through intermediary layers transparently (proxies/load balancers).
10What are key REST API best practices?hard
Answer: Use proper methods/status codes, validate input, version APIs, and provide clear errors.

10 REST API MCQs

1

Which method is typically used to create a resource?

AGET
BPOST
CDELETE
DHEAD
Explanation: POST is used to create new resources.
2

Which status code means "Not Found"?

A200
B201
C404
D500
Explanation: 404 indicates missing route/resource.
3

REST statelessness means:

AServer stores all client sessions
BEach request contains needed context
CNo headers are allowed
DOnly GET requests allowed
Explanation: Stateless APIs do not rely on prior request memory.
4

Best method for partial updates?

APUT
BPATCH
CTRACE
DCONNECT
Explanation: PATCH is intended for partial modifications.
5

Which code is best for successful creation?

A200
B201
C204
D301
Explanation: 201 Created confirms a new resource was created.
6

Which is a good REST URL style?

A/getUsers
B/users
C/usersAction
D/doCreateUser
Explanation: REST prefers nouns like /users.
7

Query params are mainly used for:

AAuth token storage only
BFiltering and pagination
CServer restart
DSetting status codes
Explanation: Query strings are used for optional controls like page/filter/sort.
8

Which response header tells payload type?

AContent-Type
BETag
COrigin
DReferrer
Explanation: Content-Type defines response media format.
9

Why version APIs?

ATo break all clients at once
BTo evolve safely without breaking consumers
COnly for authentication
DTo avoid status codes
Explanation: Versioning preserves backward compatibility.
10

Best response for malformed JSON body?

A200
B204
C400
D304
Explanation: 400 Bad Request is appropriate for invalid request format.