Skip to main content

REST (Representational State Transfer)

REST is an architectural style for designing networked applications that emphasizes simplicity, scalability, and statelessness. It leverages standard HTTP methods and status codes to create predictable, cacheable APIs that are easy to understand and integrate with.

Overview

REST treats resources as the fundamental abstraction, accessed through URLs and manipulated using standard HTTP methods. This approach creates self-describing APIs that follow web standards and can be easily consumed by any HTTP client.

Core Principles

  1. Client-Server Architecture: Clear separation between client and server concerns
  2. Statelessness: Each request contains all necessary information
  3. Cacheability: Responses can be cached to improve performance
  4. Uniform Interface: Consistent way to interact with resources
  5. Layered System: Architecture can be composed of hierarchical layers
  6. Code on Demand (optional): Server can send executable code to clients

HTTP Methods and Their Uses

GET    /api/users       # Retrieve all users
GET /api/users/123 # Retrieve user with ID 123
POST /api/users # Create a new user
PUT /api/users/123 # Replace user 123 completely
PATCH /api/users/123 # Partially update user 123
DELETE /api/users/123 # Delete user 123

When to Use REST

Ideal for:

  • Public APIs: Easy to document and consume
  • CRUD Operations: Maps naturally to resource manipulation
  • Web Applications: Leverages HTTP caching and standard tooling
  • Microservices: Simple service-to-service communication
  • Mobile Applications: Lightweight and well-supported
  • Third-party Integrations: Industry standard with broad tooling support

Consider Alternatives When:

  • Real-time Requirements: Use WebSockets or Server-Sent Events
  • Complex Queries: GraphQL might be more efficient
  • High-Performance Internal Communication: gRPC could be better
  • Event-Driven Architecture: Message queues might be more appropriate
  • Binary Data Streaming: Custom TCP protocols may be more suitable

Implementation Guidelines

Resource Design

// Good: Noun-based resources
/api/users // Collection of users
/api/users/123 // Specific user
/api/users/123/orders // User's orders
/api/orders/456 // Specific order

// Avoid: Verb-based endpoints
/api/getUserById/123 // Better as GET /api/users/123
/api/createUser // Better as POST /api/users
/api/deleteOrder/456 // Better as DELETE /api/orders/456

Status Code Usage

// Success Responses
200 OK // GET, PUT, PATCH successful
201 Created // POST successful
204 No Content // DELETE successful, PUT/PATCH with no response body

// Client Error Responses
400 Bad Request // Invalid request format
401 Unauthorized // Authentication required
403 Forbidden // Authentication insufficient
404 Not Found // Resource doesn't exist
409 Conflict // Resource state conflict
422 Unprocessable // Validation errors

// Server Error Responses
500 Internal Error // Unexpected server error
502 Bad Gateway // Upstream service error
503 Service Unavailable // Temporary unavailability

Request/Response Structure

// Consistent response envelope
interface APIResponse<T> {
data: T;
meta?: {
pagination?: {
page: number;
pageSize: number;
total: number;
totalPages: number;
};
timestamp: string;
requestId: string;
};
errors?: Array<{
code: string;
message: string;
field?: string;
}>;
}

// Example success response
{
"data": {
"id": "123",
"email": "user@example.com",
"name": "John Doe",
"createdAt": "2024-01-15T10:30:00Z"
},
"meta": {
"timestamp": "2024-01-15T10:30:00Z",
"requestId": "req_abc123"
}
}

// Example error response
{
"errors": [
{
"code": "VALIDATION_ERROR",
"message": "Email address is required",
"field": "email"
}
],
"meta": {
"timestamp": "2024-01-15T10:30:00Z",
"requestId": "req_def456"
}
}

Implementation Examples

Basic Express.js API

import express from 'express';
import { body, param, validationResult } from 'express-validator';

const app = express();
app.use(express.json());

// GET /api/users - List users with pagination
app.get('/api/users', async (req, res) => {
const page = parseInt(req.query.page as string) || 1;
const pageSize = parseInt(req.query.pageSize as string) || 20;
const offset = (page - 1) * pageSize;

try {
const users = await User.findAll({
limit: pageSize,
offset: offset,
order: [['createdAt', 'DESC']]
});

const total = await User.count();

res.json({
data: users,
meta: {
pagination: {
page,
pageSize,
total,
totalPages: Math.ceil(total / pageSize)
},
timestamp: new Date().toISOString(),
requestId: req.headers['x-request-id']
}
});
} catch (error) {
res.status(500).json({
errors: [{ code: 'INTERNAL_ERROR', message: 'Failed to fetch users' }]
});
}
});

// POST /api/users - Create user
app.post('/api/users', [
body('email').isEmail().normalizeEmail(),
body('name').trim().isLength({ min: 1, max: 100 }),
body('password').isLength({ min: 8 })
], async (req, res) => {
const errors = validationResult(req);

if (!errors.isEmpty()) {
return res.status(422).json({
errors: errors.array().map(err => ({
code: 'VALIDATION_ERROR',
message: err.msg,
field: err.param
}))
});
}

try {
const user = await User.create({
email: req.body.email,
name: req.body.name,
password: await hashPassword(req.body.password)
});

res.status(201).json({
data: {
id: user.id,
email: user.email,
name: user.name,
createdAt: user.createdAt
},
meta: {
timestamp: new Date().toISOString(),
requestId: req.headers['x-request-id']
}
});
} catch (error) {
if (error.code === 'UNIQUE_VIOLATION') {
res.status(409).json({
errors: [{ code: 'EMAIL_EXISTS', message: 'Email already registered' }]
});
} else {
res.status(500).json({
errors: [{ code: 'INTERNAL_ERROR', message: 'Failed to create user' }]
});
}
}
});

// GET /api/users/:id - Get specific user
app.get('/api/users/:id', [
param('id').isUUID()
], async (req, res) => {
const errors = validationResult(req);

if (!errors.isEmpty()) {
return res.status(400).json({
errors: [{ code: 'INVALID_ID', message: 'Invalid user ID format' }]
});
}

try {
const user = await User.findByPk(req.params.id);

if (!user) {
return res.status(404).json({
errors: [{ code: 'USER_NOT_FOUND', message: 'User not found' }]
});
}

res.json({
data: {
id: user.id,
email: user.email,
name: user.name,
createdAt: user.createdAt,
updatedAt: user.updatedAt
}
});
} catch (error) {
res.status(500).json({
errors: [{ code: 'INTERNAL_ERROR', message: 'Failed to fetch user' }]
});
}
});

// PUT /api/users/:id - Replace user
app.put('/api/users/:id', [
param('id').isUUID(),
body('email').isEmail().normalizeEmail(),
body('name').trim().isLength({ min: 1, max: 100 })
], async (req, res) => {
// Validation and update logic...
});

// DELETE /api/users/:id - Delete user
app.delete('/api/users/:id', [
param('id').isUUID()
], async (req, res) => {
try {
const deleted = await User.destroy({
where: { id: req.params.id }
});

if (deleted === 0) {
return res.status(404).json({
errors: [{ code: 'USER_NOT_FOUND', message: 'User not found' }]
});
}

res.status(204).send(); // No content
} catch (error) {
res.status(500).json({
errors: [{ code: 'INTERNAL_ERROR', message: 'Failed to delete user' }]
});
}
});

Advanced Features

Content Negotiation

// Support multiple response formats
app.get('/api/users/:id', async (req, res) => {
const user = await User.findByPk(req.params.id);

res.format({
'application/json': () => {
res.json({ data: user });
},
'application/xml': () => {
res.send(`<user><id>${user.id}</id><name>${user.name}</name></user>`);
},
'text/csv': () => {
res.send(`id,name,email\n${user.id},${user.name},${user.email}`);
}
});
});

HATEOAS (Hypermedia as the Engine of Application State)

// Include navigation links in responses
{
"data": {
"id": "123",
"name": "John Doe",
"email": "john@example.com"
},
"links": {
"self": "/api/users/123",
"orders": "/api/users/123/orders",
"edit": "/api/users/123",
"delete": "/api/users/123"
}
}

Conditional Requests

// Support ETag for caching
app.get('/api/users/:id', async (req, res) => {
const user = await User.findByPk(req.params.id);
const etag = generateETag(user);

res.set('ETag', etag);

if (req.get('If-None-Match') === etag) {
return res.status(304).end(); // Not Modified
}

res.json({ data: user });
});

// Support conditional updates
app.put('/api/users/:id', async (req, res) => {
const ifMatch = req.get('If-Match');

if (ifMatch) {
const user = await User.findByPk(req.params.id);
if (generateETag(user) !== ifMatch) {
return res.status(412).json({
errors: [{ code: 'PRECONDITION_FAILED', message: 'Resource has been modified' }]
});
}
}

// Proceed with update...
});

Best Practices

1. API Versioning

# URL versioning (recommended for breaking changes)
GET /api/v1/users
GET /api/v2/users

# Header versioning (for non-breaking changes)
GET /api/users
Accept: application/vnd.api+json;version=1

2. Filtering and Sorting

# Filtering
GET /api/users?status=active&role=admin
GET /api/users?createdAt[gte]=2024-01-01

# Sorting
GET /api/users?sort=name
GET /api/users?sort=-createdAt,name # Descending by createdAt, then ascending by name

# Field selection
GET /api/users?fields=id,name,email

3. Rate Limiting

import rateLimit from 'express-rate-limit';

const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
message: {
errors: [{ code: 'RATE_LIMIT_EXCEEDED', message: 'Too many requests' }]
},
standardHeaders: true,
legacyHeaders: false
});

app.use('/api/', limiter);

4. Authentication and Authorization

// JWT middleware
const authenticateJWT = (req, res, next) => {
const authHeader = req.headers.authorization;

if (authHeader) {
const token = authHeader.split(' ')[1];

jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) {
return res.status(403).json({
errors: [{ code: 'INVALID_TOKEN', message: 'Invalid or expired token' }]
});
}

req.user = user;
next();
});
} else {
res.status(401).json({
errors: [{ code: 'NO_TOKEN', message: 'Authorization token required' }]
});
}
};

// Protected route
app.get('/api/users/me', authenticateJWT, async (req, res) => {
const user = await User.findByPk(req.user.id);
res.json({ data: user });
});

5. Error Handling

// Global error handler
app.use((error, req, res, next) => {
console.error(error);

if (error.name === 'ValidationError') {
res.status(422).json({
errors: Object.values(error.errors).map(err => ({
code: 'VALIDATION_ERROR',
message: err.message,
field: err.path
}))
});
} else if (error.name === 'UnauthorizedError') {
res.status(401).json({
errors: [{ code: 'UNAUTHORIZED', message: 'Invalid token' }]
});
} else {
res.status(500).json({
errors: [{ code: 'INTERNAL_ERROR', message: 'Something went wrong' }]
});
}
});

Documentation Standards

OpenAPI/Swagger Specification

openapi: 3.0.0
info:
title: Users API
version: 1.0.0
paths:
/api/users:
get:
summary: List users
parameters:
- name: page
in: query
schema:
type: integer
default: 1
- name: pageSize
in: query
schema:
type: integer
default: 20
responses:
'200':
description: List of users
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/User'
post:
summary: Create user
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateUserRequest'
responses:
'201':
description: User created
components:
schemas:
User:
type: object
properties:
id:
type: string
email:
type: string
name:
type: string

Performance Considerations

1. Caching Strategy

// Response caching
app.get('/api/users/:id', cache('5 minutes'), async (req, res) => {
// Handler logic...
});

// Cache headers
res.set({
'Cache-Control': 'public, max-age=300', // 5 minutes
'ETag': generateETag(data),
'Last-Modified': data.updatedAt.toUTCString()
});

2. Pagination

// Cursor-based pagination for large datasets
app.get('/api/users', async (req, res) => {
const cursor = req.query.cursor;
const limit = parseInt(req.query.limit) || 20;

const whereClause = cursor ? { id: { [Op.gt]: cursor } } : {};

const users = await User.findAll({
where: whereClause,
limit: limit + 1, // Fetch one extra to check if there's a next page
order: [['id', 'ASC']]
});

const hasNext = users.length > limit;
const data = hasNext ? users.slice(0, -1) : users;
const nextCursor = hasNext ? users[users.length - 2].id : null;

res.json({
data,
meta: {
pagination: {
nextCursor,
hasNext
}
}
});
});

3. Compression

import compression from 'compression';

app.use(compression({
filter: (req, res) => {
if (req.headers['x-no-compression']) {
return false;
}
return compression.filter(req, res);
},
level: 6,
threshold: 1024
}));

Security Best Practices

1. Input Validation

import helmet from 'helmet';
import xss from 'xss';

app.use(helmet()); // Security headers

// Sanitize user input
const sanitizeInput = (input) => {
return xss(input, {
whiteList: {}, // No HTML tags allowed
stripIgnoreTag: true,
stripIgnoreTagBody: ['script']
});
};

2. CORS Configuration

import cors from 'cors';

app.use(cors({
origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'],
credentials: true,
optionsSuccessStatus: 200
}));

3. Request Size Limits

app.use(express.json({ 
limit: '10mb',
verify: (req, res, buf) => {
// Store raw body for webhook verification if needed
req.rawBody = buf;
}
}));

Testing REST APIs

Unit Testing with Jest

describe('User API', () => {
test('GET /api/users should return paginated users', async () => {
const response = await request(app)
.get('/api/users?page=1&pageSize=10')
.expect(200);

expect(response.body.data).toHaveLength(10);
expect(response.body.meta.pagination).toEqual({
page: 1,
pageSize: 10,
total: expect.any(Number),
totalPages: expect.any(Number)
});
});

test('POST /api/users should create new user', async () => {
const userData = {
email: 'test@example.com',
name: 'Test User',
password: 'password123'
};

const response = await request(app)
.post('/api/users')
.send(userData)
.expect(201);

expect(response.body.data).toMatchObject({
email: userData.email,
name: userData.name,
id: expect.any(String)
});
});
});