REST (Representational State Transfer)
REST is an architectural style for designing networked applications, particularly web APIs. It uses HTTP methods to implement CRUD operations in a standardized, stateless, and scalable way. RESTful APIs treat everything as a resource that can be accessed and manipulated through standard HTTP verbs.
Core Principles
1. Resources and URLs
Everything in REST is a resource (users, products, orders, etc.). Each resource is identified by a unique URL. URLs should be nouns (resources), not verbs (actions).
// GOOD - Resource-based URLs (nouns)
GET /users // Get all users
GET /users/123 // Get user with ID 123
GET /users/123/posts // Get posts by user 123
GET /posts/456/comments // Get comments on post 456
// BAD - Action-based URLs (verbs)
GET /getUsers // Don't use verbs in URLs
POST /createUser // The HTTP method is the verb
GET /deleteUser/123 // Use DELETE method instead
GET /user/get/123 // Redundant
// Resource hierarchy shows relationships
/organizations/1/departments/2/employees/3
// This URL represents:
// - Organization with ID 1
// - Department with ID 2 (within that organization)
// - Employee with ID 3 (within that department) 2. HTTP Methods Map to CRUD
REST uses HTTP methods (verbs) to indicate the operation being performed on a resource. This provides a standardized way to implement CRUD.
// Express.js RESTful API
const express = require('express');
const app = express();
app.use(express.json());
// CREATE - POST creates new resource
app.post('/users', async (req, res) => {
const newUser = {
id: generateId(),
name: req.body.name,
email: req.body.email,
createdAt: new Date()
};
await db.users.insert(newUser);
// 201 Created with Location header pointing to new resource
res.status(201)
.location(`/users/${newUser.id}`)
.json(newUser);
});
// READ - GET retrieves resources (safe, idempotent)
app.get('/users', async (req, res) => {
const users = await db.users.findAll();
res.json(users); // 200 OK
});
app.get('/users/:id', async (req, res) => {
const user = await db.users.findById(req.params.id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user); // 200 OK
});
// UPDATE - PUT replaces entire resource (idempotent)
app.put('/users/:id', async (req, res) => {
const user = await db.users.findById(req.params.id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
// Replace entire resource
const updatedUser = {
id: req.params.id,
name: req.body.name,
email: req.body.email,
updatedAt: new Date()
};
await db.users.update(req.params.id, updatedUser);
res.json(updatedUser); // 200 OK
});
// UPDATE - PATCH partially updates resource (idempotent)
app.patch('/users/:id', async (req, res) => {
const user = await db.users.findById(req.params.id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
// Merge changes
const updatedUser = {
...user,
...req.body,
updatedAt: new Date()
};
await db.users.update(req.params.id, updatedUser);
res.json(updatedUser); // 200 OK
});
// DELETE - DELETE removes resource (idempotent)
app.delete('/users/:id', async (req, res) => {
const user = await db.users.findById(req.params.id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
await db.users.delete(req.params.id);
res.status(204).send(); // 204 No Content
}); 3. Statelessness
Each request from client to server must contain all the information needed to understand and process the request. The server doesn't store client state between requests.
// STATELESS - Each request is independent
// Client includes authentication token with EVERY request
const response = await fetch('/api/users', {
headers: {
'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIs...',
'Content-Type': 'application/json'
}
});
// Server doesn't remember previous requests
app.get('/users', authenticate, (req, res) => {
// Extract user from token (not from server session)
const currentUser = verifyToken(req.headers.authorization);
// Process request with all needed information
const users = db.users.findAll({ organizationId: currentUser.orgId });
res.json(users);
});
// STATEFUL (NOT REST) - Server stores session
// Don't do this in RESTful APIs:
app.get('/login', (req, res) => {
req.session.user = authenticatedUser; // Stores state on server
});
app.get('/users', (req, res) => {
const user = req.session.user; // Relies on stored state
// This violates statelessness
}); 4. HTTP Status Codes
RESTful APIs use standard HTTP status codes to indicate the result of operations. This provides a consistent way to communicate success, errors, and edge cases.
// Success Codes (2xx)
res.status(200).json(data); // OK - Request succeeded
res.status(201).json(newResource); // Created - Resource created
res.status(204).send(); // No Content - Success, no body
// Client Error Codes (4xx)
res.status(400).json({ error: 'Invalid input' }); // Bad Request
res.status(401).json({ error: 'Unauthorized' }); // Not authenticated
res.status(403).json({ error: 'Forbidden' }); // Not authorized
res.status(404).json({ error: 'Not found' }); // Resource doesn't exist
res.status(409).json({ error: 'Conflict' }); // Duplicate/conflict
res.status(422).json({ error: 'Validation failed' }); // Unprocessable
// Server Error Codes (5xx)
res.status(500).json({ error: 'Internal server error' });
res.status(503).json({ error: 'Service unavailable' });
// Example with proper status codes
app.post('/users', async (req, res) => {
try {
// Validate input
if (!req.body.email) {
return res.status(400).json({ error: 'Email is required' });
}
// Check for duplicates
const existing = await db.users.findByEmail(req.body.email);
if (existing) {
return res.status(409).json({ error: 'Email already exists' });
}
// Create resource
const newUser = await db.users.create(req.body);
// Return 201 Created
res.status(201)
.location(`/users/${newUser.id}`)
.json(newUser);
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Internal server error' });
}
}); REST in Practice
Query Parameters for Filtering and Pagination
Use query parameters to filter, sort, and paginate collections without creating new endpoints.
// Filtering
app.get('/users', async (req, res) => {
const { role, status, search } = req.query;
let query = db.users.createQuery();
if (role) {
query = query.where('role', role);
}
if (status) {
query = query.where('status', status);
}
if (search) {
query = query.where('name', 'LIKE', `%${search}%`);
}
const users = await query.execute();
res.json(users);
});
// Usage:
// GET /users?role=admin
// GET /users?status=active&role=admin
// GET /users?search=alice
// Sorting
app.get('/users', async (req, res) => {
const { sort = 'createdAt', order = 'desc' } = req.query;
const users = await db.users
.orderBy(sort, order)
.execute();
res.json(users);
});
// Usage:
// GET /users?sort=name&order=asc
// GET /users?sort=createdAt&order=desc
// Pagination
app.get('/users', async (req, res) => {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const offset = (page - 1) * limit;
const [users, total] = await Promise.all([
db.users.findAll({ limit, offset }),
db.users.count()
]);
res.json({
data: users,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit)
}
});
});
// Usage:
// GET /users?page=1&limit=20
// GET /users?page=2&limit=20 Nested Resources
When resources have relationships, URLs can reflect the hierarchy. However, avoid deeply nested URLs (2 levels maximum is a good rule).
// User's posts
app.get('/users/:userId/posts', async (req, res) => {
const posts = await db.posts.findByUserId(req.params.userId);
res.json(posts);
});
// Specific post by a user
app.get('/users/:userId/posts/:postId', async (req, res) => {
const post = await db.posts.findOne({
id: req.params.postId,
userId: req.params.userId
});
if (!post) {
return res.status(404).json({ error: 'Post not found' });
}
res.json(post);
});
// AVOID deeply nested URLs (hard to maintain)
// BAD: GET /organizations/1/departments/2/teams/3/members/4/tasks/5
// BETTER: Use top-level resource with filters
// GET /tasks/5
// GET /tasks?memberId=4
// GET /tasks?teamId=3 Content Negotiation
REST supports multiple representations of the same resource. Clients can request different formats using the Accept header.
app.get('/users/:id', async (req, res) => {
const user = await db.users.findById(req.params.id);
if (!user) {
return res.status(404).json({ error: 'Not found' });
}
// Check what format the client wants
const acceptHeader = req.headers.accept;
if (acceptHeader.includes('application/json')) {
res.json(user);
} else if (acceptHeader.includes('application/xml')) {
res.type('application/xml');
res.send(`
<user>
<id>${user.id}</id>
<name>${user.name}</name>
<email>${user.email}</email>
</user>
`);
} else if (acceptHeader.includes('text/html')) {
res.send(`
<html>
<body>
<h1>${user.name}</h1>
<p>Email: ${user.email}</p>
</body>
</html>
`);
} else {
// Default to JSON
res.json(user);
}
});
// Client specifies format:
// Accept: application/json
// Accept: application/xml
// Accept: text/html Client-Side REST Consumption
// Modern fetch API for REST
class UserAPI {
constructor(baseUrl) {
this.baseUrl = baseUrl;
}
async getHeaders() {
return {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.getToken()}`
};
}
// CREATE
async createUser(userData) {
const response = await fetch(`${this.baseUrl}/users`, {
method: 'POST',
headers: await this.getHeaders(),
body: JSON.stringify(userData)
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
}
// READ (collection)
async getUsers(filters = {}) {
const params = new URLSearchParams(filters);
const response = await fetch(`${this.baseUrl}/users?${params}`, {
headers: await this.getHeaders()
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
}
// READ (single)
async getUser(id) {
const response = await fetch(`${this.baseUrl}/users/${id}`, {
headers: await this.getHeaders()
});
if (response.status === 404) {
return null;
}
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
}
// UPDATE (full)
async updateUser(id, userData) {
const response = await fetch(`${this.baseUrl}/users/${id}`, {
method: 'PUT',
headers: await this.getHeaders(),
body: JSON.stringify(userData)
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
}
// UPDATE (partial)
async patchUser(id, partialData) {
const response = await fetch(`${this.baseUrl}/users/${id}`, {
method: 'PATCH',
headers: await this.getHeaders(),
body: JSON.stringify(partialData)
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
}
// DELETE
async deleteUser(id) {
const response = await fetch(`${this.baseUrl}/users/${id}`, {
method: 'DELETE',
headers: await this.getHeaders()
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
// 204 No Content - no body to parse
return response.status === 204 ? null : response.json();
}
getToken() {
return localStorage.getItem('auth_token');
}
}
// Usage
const api = new UserAPI('https://api.example.com');
// Create
const newUser = await api.createUser({
name: 'Alice',
email: 'alice@example.com'
});
// Read
const users = await api.getUsers({ role: 'admin', page: 1 });
const user = await api.getUser(123);
// Update
await api.updateUser(123, { name: 'Alice Johnson', email: 'alice@example.com' });
await api.patchUser(123, { name: 'Alice Johnson' }); // Only update name
// Delete
await api.deleteUser(123);
REST vs CRUD Comparison
| Aspect | CRUD | REST |
|---|---|---|
| Concept | Four data operations | Architectural style for APIs |
| Create | Create operation | POST /resources |
| Read | Read operation | GET /resources or GET /resources/:id |
| Update | Update operation | PUT or PATCH /resources/:id |
| Delete | Delete operation | DELETE /resources/:id |
| Transport | Can use any method | Specifically uses HTTP |
| State | Can be stateful | Must be stateless |
| Resources | Any data store | URL-addressable resources |
Benefits of REST
- Standardization: Common HTTP methods mean predictable APIs
- Statelessness: Easy to scale horizontally (add more servers)
- Cacheability: GET requests can be cached by browsers and CDNs
- Client-Server Separation: Frontend and backend can evolve independently
- Platform Independence: Any HTTP client can consume a REST API
- Layered System: Can add proxies, load balancers, caches transparently
- Visibility: Requests are self-descriptive and easy to monitor
Challenges and Considerations
- Over-Fetching: May retrieve more data than needed
- Under-Fetching: May require multiple requests to get all needed data
- N+1 Problem: Nested resources can cause many round trips
- Not Always CRUD: Some operations don't map cleanly (e.g., "login", "search")
- Versioning: How to handle breaking changes (e.g., /v1/users, /v2/users)
- Real-Time: REST is request-response, not great for real-time updates
- File Uploads: Requires special handling (multipart/form-data)
RESTful Design Best Practices
- Use nouns for resource names, not verbs
- Use plural nouns for collections (
/usersnot/user) - Use proper HTTP methods (don't use GET for mutations)
- Use proper status codes consistently
- Support filtering, sorting, and pagination for collections
- Version your API (
/v1/users) - Provide clear error messages with error codes
- Document your API (OpenAPI/Swagger)
- Use HTTPS for security
- Implement rate limiting to prevent abuse
- Include pagination metadata in responses
- Use
Content-TypeandAcceptheaders correctly
When REST Isn't Ideal
// Some operations don't fit CRUD/REST well:
// 1. Actions that aren't about a resource
POST /api/login // Not really creating a "login" resource
POST /api/logout // Not really creating a "logout" resource
POST /api/search // Search isn't CRUD
POST /api/calculate // Computation, not data
// Solutions:
// Option 1: Treat action result as a resource
POST /api/sessions // Create a session (login)
DELETE /api/sessions/123 // Delete a session (logout)
POST /api/search-results // Create a search result
POST /api/calculations // Create a calculation
// Option 2: Use sub-resources for actions
POST /api/users/123/activate
POST /api/orders/456/cancel
POST /api/posts/789/publish
// Option 3: Accept that not everything is REST
// It's okay to have some RPC-style endpoints
POST /api/send-email
POST /api/generate-report
POST /api/process-payment Key Takeaways
- REST is an architectural style that uses HTTP methods to implement CRUD over the web
- Resources are identified by URLs; operations are indicated by HTTP methods
- REST APIs are stateless - each request contains all needed information
- HTTP status codes communicate the result of operations
- Use nouns for resources, use HTTP methods as verbs
- Support filtering, sorting, and pagination with query parameters
- REST maps directly to CRUD but is specifically for HTTP-based APIs
- Not all operations fit perfectly into REST - that's okay
See Also
- CRUD Architecture - The four fundamental data operations
- Client-Server Architecture - REST is a client-server pattern
- Single Page Application - SPAs often consume REST APIs