REST API Design Principles: Building Scalable Web Services
Table of Contents
- Introduction to REST
- Core REST Principles
- HTTP Methods and Status Codes
- URL Design and Resource Naming
- Request and Response Structure
- Authentication and Authorization
- Error Handling
- Versioning Strategies
- Caching and Performance
- Security Best Practices
- Documentation and Testing
Introduction to REST {#introduction}
Representational State Transfer (REST) is an architectural style for designing networked applications, particularly web services. REST was introduced by Roy Fielding in his doctoral dissertation in 2000 and has become the standard for building web APIs.
What is REST?
REST is not a protocol or standard, but rather a set of architectural principles that define how web standards like HTTP should be used to create web services.
Benefits of RESTful Design
- Simplicity: Easy to understand and implement
- Scalability: Stateless nature enables horizontal scaling
- Flexibility: Platform and language agnostic
- Performance: Leverages HTTP caching mechanisms
- Visibility: Clear separation between client and server
REST vs. Other Paradigms
// REST: Resource-oriented
GET /api/users/123
PUT /api/users/123
DELETE /api/users/123
// RPC: Action-oriented
POST /api/getUser
POST /api/updateUser
POST /api/deleteUser
// GraphQL: Query-oriented
POST /graphql
{
query: "{ user(id: 123) { name, email } }"
}
Core REST Principles {#principles}
1. Uniform Interface
The uniform interface constraint defines the interface between clients and servers:
Resource Identification
Resources are identified by URIs:
GET /api/v1/users/123
GET /api/v1/orders/456
GET /api/v1/products?category=electronics
Resource Manipulation through Representations
Resources are manipulated through their representations (JSON, XML, etc.):
{
"id": 123,
"name": "John Doe",
"email": "john@example.com",
"created_at": "2023-01-15T10:30:00Z"
}
Self-Descriptive Messages
Each message includes enough information to describe how to process it:
GET /api/v1/users/123 HTTP/1.1
Host: api.example.com
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 156
Cache-Control: max-age=3600
{
"id": 123,
"name": "John Doe",
"email": "john@example.com"
}
Hypermedia as the Engine of Application State (HATEOAS)
Responses include links to related resources:
{
"id": 123,
"name": "John Doe",
"email": "john@example.com",
"_links": {
"self": { "href": "/api/v1/users/123" },
"orders": { "href": "/api/v1/users/123/orders" },
"profile": { "href": "/api/v1/users/123/profile" }
}
}
2. Stateless
Each request must contain all information needed to understand and process it:
// Bad: Server-side session state
// Server stores user session information
// Good: Stateless with JWT token
const headers = {
'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
'Content-Type': 'application/json'
};
3. Cacheable
Responses must define themselves as cacheable or non-cacheable:
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: public, max-age=3600
ETag: "abc123"
Last-Modified: Wed, 21 Oct 2023 07:28:00 GMT
4. Client-Server
Clear separation of concerns between client and server:
- Client handles user interface and user experience
- Server handles data storage and business logic
- They can evolve independently
5. Layered System
Architecture can be composed of hierarchical layers:
Client → Load Balancer → API Gateway → Authentication Service → Business Logic → Database
6. Code on Demand (Optional)
Server can extend client functionality by transferring executable code:
{
"data": { ... },
"scripts": [
"/js/validation.js",
"/js/formatting.js"
]
}
HTTP Methods and Status Codes {#http-methods}
HTTP Methods (Verbs)
GET - Retrieve Resources
GET /api/v1/users
GET /api/v1/users/123
GET /api/v1/users/123/orders
Response: 200 OK
{
"users": [
{"id": 1, "name": "Alice"},
{"id": 2, "name": "Bob"}
],
"total": 2,
"page": 1,
"per_page": 10
}
POST - Create Resources
POST /api/v1/users
Content-Type: application/json
{
"name": "John Doe",
"email": "john@example.com"
}
Response: 201 Created
Location: /api/v1/users/123
{
"id": 123,
"name": "John Doe",
"email": "john@example.com",
"created_at": "2023-01-15T10:30:00Z"
}
PUT - Update/Replace Resources
PUT /api/v1/users/123
Content-Type: application/json
{
"name": "John Smith",
"email": "john.smith@example.com"
}
Response: 200 OK
{
"id": 123,
"name": "John Smith",
"email": "john.smith@example.com",
"updated_at": "2023-01-15T11:30:00Z"
}
PATCH - Partial Update
PATCH /api/v1/users/123
Content-Type: application/json
{
"email": "newemail@example.com"
}
Response: 200 OK
{
"id": 123,
"name": "John Doe",
"email": "newemail@example.com",
"updated_at": "2023-01-15T11:45:00Z"
}
DELETE - Remove Resources
DELETE /api/v1/users/123
Response: 204 No Content
HTTP Status Codes
Success Codes (2xx)
// 200 OK - General success
// 201 Created - Resource created successfully
// 202 Accepted - Request accepted for processing
// 204 No Content - Success with no response body
// 206 Partial Content - Partial response (pagination)
const statusCodes = {
SUCCESS: 200,
CREATED: 201,
ACCEPTED: 202,
NO_CONTENT: 204,
PARTIAL_CONTENT: 206
};
Client Error Codes (4xx)
// 400 Bad Request - Invalid request syntax
// 401 Unauthorized - Authentication required
// 403 Forbidden - Insufficient permissions
// 404 Not Found - Resource doesn't exist
// 405 Method Not Allowed - HTTP method not supported
// 409 Conflict - Resource conflict
// 422 Unprocessable Entity - Validation errors
// 429 Too Many Requests - Rate limit exceeded
const clientErrors = {
BAD_REQUEST: 400,
UNAUTHORIZED: 401,
FORBIDDEN: 403,
NOT_FOUND: 404,
METHOD_NOT_ALLOWED: 405,
CONFLICT: 409,
UNPROCESSABLE_ENTITY: 422,
TOO_MANY_REQUESTS: 429
};
Server Error Codes (5xx)
// 500 Internal Server Error - Generic server error
// 502 Bad Gateway - Invalid response from upstream
// 503 Service Unavailable - Server temporarily unavailable
// 504 Gateway Timeout - Upstream timeout
const serverErrors = {
INTERNAL_SERVER_ERROR: 500,
BAD_GATEWAY: 502,
SERVICE_UNAVAILABLE: 503,
GATEWAY_TIMEOUT: 504
};
URL Design and Resource Naming {#url-design}
Resource Naming Conventions
Use Nouns, Not Verbs
# Good
GET /api/v1/users
POST /api/v1/users
GET /api/v1/orders/123
# Bad
GET /api/v1/getUsers
POST /api/v1/createUser
GET /api/v1/retrieveOrder/123
Use Plural Nouns for Collections
# Good
GET /api/v1/users
GET /api/v1/products
GET /api/v1/orders
# Bad
GET /api/v1/user
GET /api/v1/product
GET /api/v1/order
Use Hierarchical Structure for Relationships
# User's orders
GET /api/v1/users/123/orders
# Order's items
GET /api/v1/orders/456/items
# Product reviews
GET /api/v1/products/789/reviews
URL Structure Examples
Collection and Resource Operations
// Collection operations
const apiEndpoints = {
// Get all users
getAllUsers: 'GET /api/v1/users',
// Create new user
createUser: 'POST /api/v1/users',
// Get specific user
getUser: 'GET /api/v1/users/:id',
// Update user
updateUser: 'PUT /api/v1/users/:id',
// Partially update user
patchUser: 'PATCH /api/v1/users/:id',
// Delete user
deleteUser: 'DELETE /api/v1/users/:id'
};
Query Parameters for Filtering and Pagination
# Filtering
GET /api/v1/users?status=active&role=admin
GET /api/v1/products?category=electronics&price_min=100&price_max=500
# Sorting
GET /api/v1/users?sort=name&order=asc
GET /api/v1/products?sort=price,desc&sort=rating,desc
# Pagination
GET /api/v1/users?page=2&per_page=20
GET /api/v1/users?offset=40&limit=20
# Field selection
GET /api/v1/users?fields=id,name,email
GET /api/v1/products?include=category,reviews&exclude=internal_notes
Search Operations
# Simple search
GET /api/v1/users?q=john
# Advanced search
GET /api/v1/products?search[name]=laptop&search[category]=electronics
# Full-text search
GET /api/v1/articles?query=javascript&highlight=true
Path Parameters vs Query Parameters
Path Parameters (Resource Identification)
// Path parameters identify specific resources
app.get('/api/v1/users/:userId/orders/:orderId', (req, res) => {
const { userId, orderId } = req.params;
// Fetch specific order for specific user
});
Query Parameters (Resource Modification)
// Query parameters modify the response
app.get('/api/v1/users', (req, res) => {
const {
page = 1,
per_page = 10,
status,
sort = 'id',
order = 'asc'
} = req.query;
// Filter, sort, and paginate users
});
Request and Response Structure {#request-response}
Request Structure
Headers
POST /api/v1/users HTTP/1.1
Host: api.example.com
Content-Type: application/json
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
User-Agent: MyApp/1.0
X-Request-ID: abc123-def456-ghi789
Request Body Structure
{
"data": {
"type": "user",
"attributes": {
"name": "John Doe",
"email": "john@example.com",
"preferences": {
"language": "en",
"timezone": "UTC"
}
}
}
}
Response Structure
Consistent Response Format
{
"status": "success",
"data": {
"id": 123,
"type": "user",
"attributes": {
"name": "John Doe",
"email": "john@example.com",
"created_at": "2023-01-15T10:30:00Z"
}
},
"meta": {
"request_id": "abc123-def456-ghi789",
"timestamp": "2023-01-15T10:30:00Z"
}
}
Collection Response Format
{
"status": "success",
"data": [
{
"id": 1,
"type": "user",
"attributes": {
"name": "Alice",
"email": "alice@example.com"
}
},
{
"id": 2,
"type": "user",
"attributes": {
"name": "Bob",
"email": "bob@example.com"
}
}
],
"meta": {
"pagination": {
"current_page": 1,
"per_page": 10,
"total_pages": 5,
"total_items": 50
},
"request_id": "xyz789-abc123-def456"
}
}
Error Response Format
{
"status": "error",
"error": {
"code": "VALIDATION_ERROR",
"message": "The request contains invalid data",
"details": [
{
"field": "email",
"code": "INVALID_FORMAT",
"message": "Email must be a valid email address"
},
{
"field": "age",
"code": "OUT_OF_RANGE",
"message": "Age must be between 18 and 120"
}
]
},
"meta": {
"request_id": "error123-456789",
"timestamp": "2023-01-15T10:30:00Z"
}
}
Content Negotiation
# Client specifies preferred format
Accept: application/json
Accept: application/xml
Accept: application/json, application/xml;q=0.8
# Server responds with appropriate format
Content-Type: application/json
Content-Type: application/xml
Authentication and Authorization {#auth}
Authentication Methods
JWT (JSON Web Tokens)
// JWT Structure: header.payload.signature
const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
// Usage in requests
const headers = {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
};
API Keys
# Header-based
Authorization: ApiKey your-api-key-here
X-API-Key: your-api-key-here
# Query parameter
GET /api/v1/users?api_key=your-api-key-here
OAuth 2.0
// Authorization Code Flow
const authUrl = 'https://auth.example.com/oauth/authorize?' +
'client_id=your-client-id&' +
'redirect_uri=https://yourapp.com/callback&' +
'response_type=code&' +
'scope=read write&' +
'state=random-state-string';
// Token exchange
const tokenResponse = await fetch('https://auth.example.com/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: 'your-client-id',
client_secret: 'your-client-secret',
code: 'authorization-code-from-callback',
redirect_uri: 'https://yourapp.com/callback'
})
});
Authorization Patterns
Role-Based Access Control (RBAC)
{
"user": {
"id": 123,
"roles": ["admin", "editor"],
"permissions": [
"users:read",
"users:write",
"products:read",
"products:write"
]
}
}
Attribute-Based Access Control (ABAC)
{
"subject": {
"id": 123,
"department": "sales",
"level": "manager"
},
"resource": {
"type": "customer_data",
"owner": "sales_team",
"classification": "confidential"
},
"action": "read",
"environment": {
"time": "business_hours",
"location": "office"
}
}
Security Headers
# Request headers
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
X-Request-ID: unique-request-identifier
# Response headers
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 999
X-RateLimit-Reset: 1609459200
Error Handling {#error-handling}
Comprehensive Error Responses
Validation Errors
{
"status": "error",
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": [
{
"field": "email",
"value": "invalid-email",
"code": "INVALID_EMAIL_FORMAT",
"message": "Please provide a valid email address"
},
{
"field": "password",
"code": "PASSWORD_TOO_SHORT",
"message": "Password must be at least 8 characters long"
}
]
},
"meta": {
"request_id": "req_123456789",
"timestamp": "2023-01-15T10:30:00Z",
"documentation_url": "https://docs.api.example.com/errors#validation"
}
}
Business Logic Errors
{
"status": "error",
"error": {
"code": "INSUFFICIENT_FUNDS",
"message": "Cannot process payment due to insufficient funds",
"details": {
"account_balance": 50.00,
"required_amount": 75.00,
"currency": "USD"
}
},
"meta": {
"request_id": "req_987654321",
"timestamp": "2023-01-15T10:30:00Z"
}
}
System Errors
{
"status": "error",
"error": {
"code": "SERVICE_UNAVAILABLE",
"message": "The service is temporarily unavailable",
"details": {
"retry_after": 300,
"estimated_resolution": "2023-01-15T11:00:00Z"
}
},
"meta": {
"request_id": "req_error_001",
"timestamp": "2023-01-15T10:30:00Z"
}
}
Error Handling Implementation
// Express.js error handling middleware
app.use((error, req, res, next) => {
const errorResponse = {
status: 'error',
error: {
code: error.code || 'INTERNAL_SERVER_ERROR',
message: error.message || 'An unexpected error occurred'
},
meta: {
request_id: req.id,
timestamp: new Date().toISOString()
}
};
// Add error details for validation errors
if (error.type === 'validation') {
errorResponse.error.details = error.details;
}
// Log error for monitoring
logger.error('API Error', {
error: error.stack,
request_id: req.id,
method: req.method,
url: req.url,
user_id: req.user?.id
});
res.status(error.statusCode || 500).json(errorResponse);
});
Versioning Strategies {#versioning}
URI Versioning
GET /api/v1/users
GET /api/v2/users
GET /api/v3/users
Header Versioning
GET /api/users
Accept: application/vnd.api+json;version=1
Accept: application/vnd.api+json;version=2
Query Parameter Versioning
GET /api/users?version=1
GET /api/users?v=2
Content Type Versioning
GET /api/users
Accept: application/vnd.company.v1+json
Accept: application/vnd.company.v2+json
Versioning Best Practices
// Semantic versioning for APIs
const versions = {
v1: {
major: 1,
minor: 0,
patch: 0,
deprecated: false,
sunset_date: null
},
v2: {
major: 2,
minor: 1,
patch: 3,
deprecated: false,
sunset_date: null
},
v1_deprecated: {
major: 1,
minor: 0,
patch: 0,
deprecated: true,
sunset_date: '2024-12-31T23:59:59Z'
}
};
// Deprecation headers
const deprecationHeaders = {
'Deprecation': 'true',
'Sunset': '2024-12-31T23:59:59Z',
'Link': '<https://docs.api.example.com/migration>; rel="successor-version"'
};
Caching and Performance {#performance}
HTTP Caching Headers
Cache-Control
# Public cache, 1 hour expiration
Cache-Control: public, max-age=3600
# Private cache, must revalidate
Cache-Control: private, must-revalidate
# No caching
Cache-Control: no-cache, no-store, must-revalidate
ETag and Conditional Requests
# Initial request
GET /api/v1/users/123
Accept: application/json
# Response with ETag
HTTP/1.1 200 OK
ETag: "abc123def456"
Content-Type: application/json
# Conditional request
GET /api/v1/users/123
If-None-Match: "abc123def456"
# Not modified response
HTTP/1.1 304 Not Modified
ETag: "abc123def456"
Last-Modified and If-Modified-Since
# Response with Last-Modified
HTTP/1.1 200 OK
Last-Modified: Wed, 15 Jan 2023 10:30:00 GMT
Content-Type: application/json
# Conditional request
GET /api/v1/users/123
If-Modified-Since: Wed, 15 Jan 2023 10:30:00 GMT
# Not modified response
HTTP/1.1 304 Not Modified
Performance Optimization Techniques
Pagination
// Offset-based pagination
const paginationParams = {
page: 1,
per_page: 20,
offset: 0,
limit: 20
};
// Cursor-based pagination (better for large datasets)
const cursorParams = {
cursor: 'eyJpZCI6MTIzfQ==',
limit: 20,
direction: 'next'
};
// Response with pagination metadata
const paginatedResponse = {
data: [...],
meta: {
pagination: {
current_page: 1,
per_page: 20,
total_pages: 10,
total_items: 200,
has_next: true,
has_previous: false,
next_cursor: 'eyJpZCI6MTQzfQ==',
previous_cursor: null
}
}
};
Field Selection and Inclusion
# Select specific fields
GET /api/v1/users?fields=id,name,email
# Include related resources
GET /api/v1/users?include=profile,orders
# Exclude sensitive fields
GET /api/v1/users?exclude=internal_notes,private_data
Compression
# Request compression support
Accept-Encoding: gzip, deflate, br
# Response with compression
Content-Encoding: gzip
Vary: Accept-Encoding
Rate Limiting
// Rate limiting headers
const rateLimitHeaders = {
'X-RateLimit-Limit': '1000',
'X-RateLimit-Remaining': '999',
'X-RateLimit-Reset': '1609459200',
'X-RateLimit-Window': '3600'
};
// Rate limit exceeded response
const rateLimitResponse = {
status: 'error',
error: {
code: 'RATE_LIMIT_EXCEEDED',
message: 'API rate limit exceeded',
details: {
limit: 1000,
window: 3600,
reset_time: '2023-01-15T11:00:00Z'
}
}
};
Security Best Practices {#security}
Input Validation and Sanitization
// Input validation schema
const userSchema = {
name: {
type: 'string',
minLength: 2,
maxLength: 100,
pattern: '^[a-zA-Z\s]+$'
},
email: {
type: 'string',
format: 'email',
maxLength: 255
},
age: {
type: 'integer',
minimum: 18,
maximum: 120
}
};
// SQL injection prevention
const getUserById = async (id) => {
// Use parameterized queries
const query = 'SELECT * FROM users WHERE id = $1';
const result = await db.query(query, [id]);
return result.rows[0];
};
HTTPS and Transport Security
// Enforce HTTPS
app.use((req, res, next) => {
if (!req.secure && req.get('x-forwarded-proto') !== 'https') {
return res.redirect('https://' + req.get('host') + req.url);
}
next();
});
// Security headers
app.use((req, res, next) => {
res.set({
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY',
'X-XSS-Protection': '1; mode=block',
'Content-Security-Policy': "default-src 'self'"
});
next();
});
CORS Configuration
// CORS configuration
const corsOptions = {
origin: ['https://app.example.com', 'https://admin.example.com'],
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Request-ID'],
exposedHeaders: ['X-RateLimit-Limit', 'X-RateLimit-Remaining'],
credentials: true,
maxAge: 86400 // 24 hours
};
app.use(cors(corsOptions));
API Security Checklist
const securityChecklist = [
'✓ Use HTTPS for all endpoints',
'✓ Implement proper authentication',
'✓ Validate and sanitize all inputs',
'✓ Use parameterized queries',
'✓ Implement rate limiting',
'✓ Set security headers',
'✓ Configure CORS properly',
'✓ Log security events',
'✓ Keep dependencies updated',
'✓ Implement proper error handling',
'✓ Use least privilege principle',
'✓ Encrypt sensitive data'
];
Documentation and Testing {#documentation}
OpenAPI/Swagger Documentation
# openapi.yaml
openapi: 3.0.3
info:
title: User Management API
description: A comprehensive API for managing users
version: 1.0.0
contact:
name: API Support
email: api-support@example.com
url: https://example.com/support
servers:
- url: https://api.example.com/v1
description: Production server
- url: https://staging-api.example.com/v1
description: Staging server
paths:
/users:
get:
summary: List users
description: Retrieve a paginated list of users
parameters:
- name: page
in: query
description: Page number for pagination
schema:
type: integer
minimum: 1
default: 1
- name: per_page
in: query
description: Number of items per page
schema:
type: integer
minimum: 1
maximum: 100
default: 10
responses:
'200':
description: Successful response
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/User'
meta:
$ref: '#/components/schemas/PaginationMeta'
components:
schemas:
User:
type: object
required:
- id
- name
- email
properties:
id:
type: integer
example: 123
name:
type: string
example: "John Doe"
email:
type: string
format: email
example: "john@example.com"
created_at:
type: string
format: date-time
example: "2023-01-15T10:30:00Z"
API Testing
// Unit tests for API endpoints
describe('User API', () => {
describe('GET /api/v1/users', () => {
it('should return list of users', async () => {
const response = await request(app)
.get('/api/v1/users')
.set('Authorization', `Bearer ${validToken}`)
.expect(200);
expect(response.body).toHaveProperty('data');
expect(response.body).toHaveProperty('meta');
expect(Array.isArray(response.body.data)).toBe(true);
});
it('should support pagination', async () => {
const response = await request(app)
.get('/api/v1/users?page=2&per_page=5')
.set('Authorization', `Bearer ${validToken}`)
.expect(200);
expect(response.body.meta.pagination.current_page).toBe(2);
expect(response.body.meta.pagination.per_page).toBe(5);
});
});
describe('POST /api/v1/users', () => {
it('should create a new user', async () => {
const userData = {
name: 'Jane Doe',
email: 'jane@example.com'
};
const response = await request(app)
.post('/api/v1/users')
.set('Authorization', `Bearer ${validToken}`)
.send(userData)
.expect(201);
expect(response.body.data).toMatchObject(userData);
expect(response.body.data).toHaveProperty('id');
});
it('should validate required fields', async () => {
const invalidData = { name: 'Jane Doe' }; // Missing email
const response = await request(app)
.post('/api/v1/users')
.set('Authorization', `Bearer ${validToken}`)
.send(invalidData)
.expect(422);
expect(response.body.error.code).toBe('VALIDATION_ERROR');
});
});
});
// Integration tests
describe('User Management Flow', () => {
it('should complete user lifecycle', async () => {
// Create user
const createResponse = await request(app)
.post('/api/v1/users')
.set('Authorization', `Bearer ${validToken}`)
.send({ name: 'Test User', email: 'test@example.com' })
.expect(201);
const userId = createResponse.body.data.id;
// Retrieve user
await request(app)
.get(`/api/v1/users/${userId}`)
.set('Authorization', `Bearer ${validToken}`)
.expect(200);
// Update user
await request(app)
.put(`/api/v1/users/${userId}`)
.set('Authorization', `Bearer ${validToken}`)
.send({ name: 'Updated User', email: 'updated@example.com' })
.expect(200);
// Delete user
await request(app)
.delete(`/api/v1/users/${userId}`)
.set('Authorization', `Bearer ${validToken}`)
.expect(204);
// Verify deletion
await request(app)
.get(`/api/v1/users/${userId}`)
.set('Authorization', `Bearer ${validToken}`)
.expect(404);
});
});
This comprehensive guide covers the essential principles and practices for designing robust REST APIs. By following these guidelines, you'll create APIs that are intuitive, scalable, secure, and maintainable. Remember that REST is not just about using HTTP methods correctly, but about creating a well-designed, resource-oriented architecture that provides a great developer experience.
The key to successful API design is consistency, clear documentation, and continuous feedback from API consumers. Start with simple, clear endpoints and gradually add complexity as needed, always keeping the developer experience at the forefront of your design decisions.