Complete Node.js REST API Tutorial
A comprehensive guide to building RESTful APIs with Node.js - from theory to practice with simple examples.
Table of Contents
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?
| Benefit | Explanation | Real-world example |
|---|---|---|
| Standardized | Works the same way everywhere | Any app can talk to Twitter's API |
| Stateless | Each request is independent | Server doesn't need to "remember" you |
| Scalable | Can handle millions of requests | Google Maps API serves billions of requests |
| Flexible | Returns data in multiple formats | JSON, XML, HTML |
REST API vs Regular Website
| Regular Website | REST API |
|---|---|
| Returns HTML pages | Returns data (usually JSON) |
| Designed for humans | Designed for applications |
| Includes styling | Just raw data |
| Browser renders it | App 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.
| Action | HTTP Method | URL Example | What it does |
|---|---|---|---|
| Create | POST | /users | Add a new user |
| Read | GET | /users/1 | Get user #1 |
| Update (full) | PUT | /users/1 | Replace user #1 |
| Update (partial) | PATCH | /users/1 | Update user's email |
| Delete | DELETE | /users/1 | Remove user #1 |
| List | GET | /users | Get 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.
| Operation | HTTP Method | Endpoint | Description |
|---|---|---|---|
| Create | POST | /users | Add new user |
| Read | GET | /users | Get all users |
| Read one | GET | /users/1 | Get specific user |
| Update | PUT/PATCH | /users/1 | Modify user |
| Delete | DELETE | /users/1 | Remove 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)
| PUT | PATCH |
|---|---|
| Replace entire resource | Update specific fields |
| Send all fields | Send only changed fields |
| If field missing, it becomes null | Missing 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 Type | Best for | Example |
|---|---|---|
| In-memory | Learning/prototyping | Arrays, variables |
| File system | Small apps, config | JSON files |
| SQL database | Structured data | PostgreSQL, MySQL |
| NoSQL database | Flexible data | MongoDB |
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 Type | Status Code | When it happens |
|---|---|---|
| Bad Request | 400 | Client sends wrong data format |
| Unauthorized | 401 | No authentication provided |
| Forbidden | 403 | Authenticated but not allowed |
| Not Found | 404 | Resource doesn't exist |
| Conflict | 409 | Resource already exists |
| Validation Error | 422 | Data doesn't meet requirements |
| Server Error | 500 | Something 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
| Strategy | URL Example | Header Example |
|---|---|---|
| URL path | /v1/users | - |
| Query param | /users?version=1 | - |
| Custom header | /users | API-Version: 1 |
| Content negotiation | /users | Accept: 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
| Action | Method | URL | Request Body | Response |
|---|---|---|---|---|
| List all | GET | /items | None | [{...}] |
| Get one | GET | /items/1 | None | {...} |
| Create | POST | /items | {...} | {id: 3,...} |
| Replace | PUT | /items/1 | {...} | {...} |
| Update | PATCH | /items/1 | {changes} | {...} |
| Delete | DELETE | /items/1 | None | {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&A10 REST API MCQs
Which method is typically used to create a resource?
POST is used to create new resources.Which status code means "Not Found"?
404 indicates missing route/resource.REST statelessness means:
Best method for partial updates?
PATCH is intended for partial modifications.Which code is best for successful creation?
201 Created confirms a new resource was created.Which is a good REST URL style?
/users.Query params are mainly used for:
Which response header tells payload type?
Content-Type defines response media format.Why version APIs?
Best response for malformed JSON body?
400 Bad Request is appropriate for invalid request format.