Master modern API design, development, and integration patterns for building scalable web services
An Application Programming Interface (API) is a set of rules and protocols that allows different software applications to communicate with each other. APIs are the backbone of modern software architecture, enabling integration between systems, services, and platforms.
Think of an API as a waiter in a restaurant. You (the client) look at the menu (API documentation), order your meal (make a request), the waiter takes your order to the kitchen (server), and returns with your food (response). You don't need to know how the kitchen works internally.
Most popular, uses HTTP methods, stateless, resource-based
Use Case: Web services, CRUD operations, microservices
Query language for APIs, client specifies exactly what data it needs
Use Case: Complex data requirements, mobile apps
XML-based protocol, strict standards, enterprise-focused
Use Case: Financial services, legacy systems
High-performance RPC framework using Protocol Buffers
Use Case: Microservices, real-time communication
Full-duplex communication, persistent connection
Use Case: Real-time apps, chat, gaming
Event-driven, server pushes data to client
Use Case: Notifications, event triggers
REST (Representational State Transfer) is an architectural style for designing networked applications. It relies on stateless, client-server communication using HTTP.
Separation of concerns: UI and data storage are separated, allowing independent evolution.
Each request contains all information needed to process it. Server doesn't store client context between requests.
Responses must define themselves as cacheable or non-cacheable to improve performance.
Standard way to communicate between components:
Client cannot tell if connected directly to end server or intermediary (load balancer, cache, etc.)
Servers can extend client functionality by transferring executable code (JavaScript, etc.)
Resource-Based URL Structure:
✅ GOOD (Noun-based, hierarchical):
GET /api/v1/users # Get all users
GET /api/v1/users/123 # Get user by ID
POST /api/v1/users # Create new user
PUT /api/v1/users/123 # Update user (full)
PATCH /api/v1/users/123 # Update user (partial)
DELETE /api/v1/users/123 # Delete user
GET /api/v1/users/123/orders # Get orders for user
GET /api/v1/users/123/orders/456 # Get specific order
❌ BAD (Verb-based, inconsistent):
GET /api/getUsers
POST /api/createUser
GET /api/user/get?id=123
POST /api/deleteUser?id=123
Level 0: The Swamp of POX (Plain Old XML)
- Single URI, single HTTP method (usually POST)
- Example: Traditional SOAP/XML-RPC
Level 1: Resources
- Multiple URIs, single HTTP method
- Each resource has its own URI
Level 2: HTTP Verbs
- Multiple URIs, multiple HTTP methods
- Proper use of GET, POST, PUT, DELETE, etc.
- Appropriate status codes (200, 404, 500)
Level 3: Hypermedia Controls (HATEOAS)
- Responses include links to related resources
- Client discovers API capabilities dynamically
Most REST APIs operate at Level 2.
| Method | Purpose | Idempotent | Safe | Request Body | Response Body |
|---|---|---|---|---|---|
| GET | Retrieve resource | Yes | Yes | No | Yes |
| POST | Create resource | No | No | Yes | Yes |
| PUT | Update/Replace resource | Yes | No | Yes | Yes |
| PATCH | Partial update | No | No | Yes | Yes |
| DELETE | Delete resource | Yes | No | No | Optional |
| HEAD | Get metadata only | Yes | Yes | No | No |
| OPTIONS | Get allowed methods | Yes | Yes | No | Yes |
// Create User
POST /api/v1/users
Content-Type: application/json
{
"name": "John Doe",
"email": "john@example.com",
"role": "user"
}
Response: 201 Created
{
"id": 123,
"name": "John Doe",
"email": "john@example.com",
"role": "user",
"createdAt": "2024-01-15T10:30:00Z"
}
// Read User
GET /api/v1/users/123
Response: 200 OK
{
"id": 123,
"name": "John Doe",
"email": "john@example.com",
"role": "user",
"createdAt": "2024-01-15T10:30:00Z"
}
// Update User (Full Replace)
PUT /api/v1/users/123
Content-Type: application/json
{
"name": "John Smith",
"email": "john.smith@example.com",
"role": "admin"
}
Response: 200 OK
{
"id": 123,
"name": "John Smith",
"email": "john.smith@example.com",
"role": "admin",
"updatedAt": "2024-01-15T11:00:00Z"
}
// Partial Update
PATCH /api/v1/users/123
Content-Type: application/json
{
"role": "admin"
}
Response: 200 OK
{
"id": 123,
"name": "John Smith",
"email": "john.smith@example.com",
"role": "admin",
"updatedAt": "2024-01-15T11:05:00Z"
}
// Delete User
DELETE /api/v1/users/123
Response: 204 No Content
✅ GOOD:
GET /users
POST /users
GET /users/123
❌ BAD:
GET /getUsers
POST /createUser
GET /getUser?id=123
✅ GOOD:
/users
/products
/orders
❌ BAD:
/user
/product
/order
✅ GOOD:
GET /users/123/orders # Get all orders for user 123
GET /users/123/orders/456 # Get specific order for user 123
POST /users/123/orders # Create order for user 123
⚠️ Avoid deep nesting (max 2-3 levels):
/users/123/orders/456/items/789/reviews # Too deep!
// Filtering
GET /users?role=admin&status=active
// Sorting
GET /users?sort=created_at&order=desc
// Pagination
GET /users?page=2&limit=20
GET /users?offset=20&limit=20
// Searching
GET /users?search=john
// Field Selection
GET /users?fields=id,name,email
// Combined
GET /users?role=admin&sort=created_at&page=1&limit=10
Choose one style and stick to it:
✅ snake_case (recommended for JSON):
{
"user_id": 123,
"first_name": "John",
"created_at": "2024-01-15"
}
✅ camelCase (common in JavaScript):
{
"userId": 123,
"firstName": "John",
"createdAt": "2024-01-15"
}
❌ Inconsistent:
{
"user_id": 123,
"firstName": "John",
"created_at": "2024-01-15"
}
// Single Resource
{
"data": {
"id": 123,
"name": "John Doe"
}
}
// Collection
{
"data": [
{"id": 123, "name": "John Doe"},
{"id": 124, "name": "Jane Smith"}
],
"meta": {
"page": 1,
"per_page": 20,
"total": 150,
"total_pages": 8
},
"links": {
"self": "/users?page=1",
"next": "/users?page=2",
"last": "/users?page=8"
}
}
// Error Response
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid input data",
"details": [
{
"field": "email",
"message": "Email is required"
},
{
"field": "age",
"message": "Age must be at least 18"
}
]
}
}
Use Case: Simple authentication for server-to-server communication
GET /api/v1/users
X-API-Key: your-api-key-here
Pros:
+ Simple to implement
+ Good for public APIs
Cons:
- Less secure (keys can't expire easily)
- No user context
- Difficult to rotate
Use Case: Third-party authorization, social login
OAuth 2.0 Flow (Authorization Code):
1. Client requests authorization from user
2. User grants permission
3. Client receives authorization code
4. Client exchanges code for access token
5. Client uses access token to access API
GET /api/v1/users
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Pros:
+ Industry standard
+ Secure token-based
+ Supports multiple grant types
+ Tokens can expire
Cons:
- Complex to implement
- Requires token management
Use Case: Stateless authentication, microservices
JWT Structure: header.payload.signature
// Header
{
"alg": "HS256",
"typ": "JWT"
}
// Payload
{
"sub": "123",
"name": "John Doe",
"role": "admin",
"iat": 1516239022,
"exp": 1516242622
}
// Usage
GET /api/v1/users
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Pros:
+ Self-contained (no database lookup)
+ Stateless
+ Cross-domain/microservices friendly
+ Can include claims
Cons:
- Cannot be revoked easily
- Size (larger than session IDs)
- Security risk if not implemented correctly
Use Case: Simple internal APIs, development
GET /api/v1/users
Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=
// Base64 encoded: "username:password"
⚠️ MUST use HTTPS in production
Pros:
+ Very simple
+ Built into HTTP
Cons:
- Credentials sent with every request
- No expiration
- Less secure
Users assigned roles, roles have permissions
Roles: Admin, User, Guest
Admin can: CRUD all resources
User can: Read, Create own
Guest can: Read only
Access based on attributes (user, resource, environment)
Allow if:
- User.role = "manager"
- Resource.department = User.department
- Time = business_hours
Fine-grained permissions per user
User permissions:
- users.read
- users.create
- posts.edit.own
- posts.delete.all
Token includes scopes defining access
Token scopes:
read:users
write:posts
delete:comments
API versioning allows you to make changes without breaking existing clients. There are several strategies:
https://api.example.com/v1/users
https://api.example.com/v2/users
Pros:
+ Simple and clear
+ Easy to route
+ Visible in URLs
Cons:
- Pollutes URI space
- Multiple versions = multiple endpoints
GET /users
Accept-Version: v2
# or
API-Version: 2
Pros:
+ Clean URIs
+ RESTful
Cons:
- Less visible
- Harder to test in browser
GET /users
Accept: application/vnd.example.v2+json
Pros:
+ RESTful
+ Follows HTTP standards
Cons:
- Complex
- Harder to debug
https://api.example.com/users?version=2
Pros:
+ Easy to implement
Cons:
- Can conflict with other parameters
- Less RESTful
// Standard Error Format
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid request data",
"details": [
{
"field": "email",
"message": "Email is required",
"code": "REQUIRED_FIELD"
},
{
"field": "age",
"message": "Age must be at least 18",
"code": "MIN_VALUE",
"constraint": 18
}
],
"timestamp": "2024-01-15T10:30:00Z",
"path": "/api/v1/users",
"requestId": "abc-123-def-456"
}
}
// Simple Error (4xx)
{
"error": {
"code": "NOT_FOUND",
"message": "User with ID 123 not found"
}
}
// Server Error (5xx)
{
"error": {
"code": "INTERNAL_ERROR",
"message": "An unexpected error occurred",
"requestId": "xyz-789"
}
}
// Example: Node.js/Express Error Handler
app.use((err, req, res, next) => {
// Log error for debugging
console.error(err);
// Validation errors
if (err.name === 'ValidationError') {
return res.status(400).json({
error: {
code: 'VALIDATION_ERROR',
message: err.message,
details: err.errors
}
});
}
// Not found errors
if (err.name === 'NotFoundError') {
return res.status(404).json({
error: {
code: 'NOT_FOUND',
message: err.message
}
});
}
// Unauthorized
if (err.name === 'UnauthorizedError') {
return res.status(401).json({
error: {
code: 'UNAUTHORIZED',
message: 'Authentication required'
}
});
}
// Generic server error
res.status(500).json({
error: {
code: 'INTERNAL_ERROR',
message: 'An unexpected error occurred',
requestId: req.id
}
});
});
Always use HTTPS in production to encrypt data in transit
// Redirect HTTP to HTTPS
if (req.protocol !== 'https') {
res.redirect('https://' + req.hostname + req.url);
}
Validate all input to prevent injection attacks
// Validate and sanitize
const schema = {
email: Joi.string().email(),
age: Joi.number().min(0).max(150)
};
validate(req.body, schema);
Prevent abuse and DDoS attacks
// Rate limit: 100 requests per 15 min
const limiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100
});
app.use('/api', limiter);
Control cross-origin access
// Configure CORS
app.use(cors({
origin: 'https://example.com',
methods: ['GET', 'POST'],
credentials: true
}));
Use parameterized queries
// ❌ BAD (vulnerable)
query(`SELECT * FROM users WHERE id = ${id}`);
// ✅ GOOD (safe)
query('SELECT * FROM users WHERE id = ?', [id]);
// Essential security headers
app.use(helmet({
contentSecurityPolicy: true,
hsts: {
maxAge: 31536000,
includeSubDomains: true
},
frameguard: {
action: 'deny'
},
noSniff: true,
xssFilter: true
}));
// Headers sent:
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Industry standard for documenting REST APIs
openapi: 3.0.0
info:
title: User API
version: 1.0.0
description: API for managing users
paths:
/users:
get:
summary: Get all users
parameters:
- name: page
in: query
schema:
type: integer
default: 1
- name: limit
in: query
schema:
type: integer
default: 20
responses:
'200':
description: Successful response
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/User'
post:
summary: Create a new user
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UserInput'
responses:
'201':
description: User created
content:
application/json:
schema:
$ref: '#/components/schemas/User'
components:
schemas:
User:
type: object
properties:
id:
type: integer
name:
type: string
email:
type: string
format: email
createdAt:
type: string
format: date-time
Interactive documentation from OpenAPI spec
Features: Try it out, code generation
API development and documentation
Features: Collections, testing, mock servers
Beautiful OpenAPI documentation
Features: Responsive, customizable themes
Markdown-based API documentation
Features: Simple syntax, readable
Test individual API functions/handlers in isolation
// Jest example
describe('User API', () => {
test('GET /users returns user list', async () => {
const response = await request(app)
.get('/api/v1/users')
.expect(200);
expect(response.body.data).toBeInstanceOf(Array);
expect(response.body.data.length).toBeGreaterThan(0);
});
test('POST /users creates new user', async () => {
const newUser = {
name: 'John Doe',
email: 'john@example.com'
};
const response = await request(app)
.post('/api/v1/users')
.send(newUser)
.expect(201);
expect(response.body.data).toHaveProperty('id');
expect(response.body.data.name).toBe(newUser.name);
});
test('GET /users/999 returns 404', async () => {
await request(app)
.get('/api/v1/users/999')
.expect(404);
});
});
Test API endpoints with real database and dependencies
describe('User Integration Tests', () => {
beforeAll(async () => {
await db.connect();
});
afterAll(async () => {
await db.disconnect();
});
beforeEach(async () => {
await db.clearUsers();
});
test('Complete user lifecycle', async () => {
// Create user
const createResponse = await request(app)
.post('/api/v1/users')
.send({ name: 'John', email: 'john@test.com' })
.expect(201);
const userId = createResponse.body.data.id;
// Retrieve user
const getResponse = await request(app)
.get(`/api/v1/users/${userId}`)
.expect(200);
expect(getResponse.body.data.name).toBe('John');
// Update user
await request(app)
.patch(`/api/v1/users/${userId}`)
.send({ name: 'John Updated' })
.expect(200);
// Delete user
await request(app)
.delete(`/api/v1/users/${userId}`)
.expect(204);
// Verify deletion
await request(app)
.get(`/api/v1/users/${userId}`)
.expect(404);
});
});
Verify API meets contract/specification
Test API under load and stress conditions
GraphQL is a query language for APIs that allows clients to request exactly the data they need.
// GraphQL Query
query {
user(id: 123) {
id
name
email
posts {
id
title
comments {
id
text
}
}
}
}
// Response (only requested fields)
{
"data": {
"user": {
"id": 123,
"name": "John Doe",
"email": "john@example.com",
"posts": [
{
"id": 1,
"title": "First Post",
"comments": [...]
}
]
}
}
}
| Aspect | REST | GraphQL |
|---|---|---|
| Data Fetching | Multiple endpoints, fixed responses | Single endpoint, flexible queries |
| Over-fetching | Common (get unnecessary data) | No (request only what you need) |
| Under-fetching | Common (multiple requests needed) | Rare (single query for nested data) |
| Versioning | Explicit versions (v1, v2) | No versioning needed (evolve schema) |
| Caching | HTTP caching (easier) | More complex (requires custom logic) |
| Learning Curve | Lower (familiar HTTP concepts) | Higher (new query language) |
| Tooling | Mature, widespread | Growing, less mature |
| Best For | Simple CRUD, public APIs, caching needs | Complex data, mobile apps, rapid iteration |
// Cache-Control headers
Cache-Control: public, max-age=3600
ETag: "33a64df551425fcc55e"
// Client sends:
If-None-Match: "33a64df551425fcc55e"
// Server responds:
304 Not Modified
// Cache frequently accessed data
const cachedUser = await redis.get(`user:${id}`);
if (cachedUser) {
return JSON.parse(cachedUser);
}
const user = await db.getUser(id);
await redis.set(`user:${id}`, JSON.stringify(user), 'EX', 3600);
return user;
Use CDN for static API responses and assets
// Cache query results
const cacheKey = 'users:active';
let users = cache.get(cacheKey);
if (!users) {
users = await db.query('SELECT * FROM users WHERE active = true');
cache.set(cacheKey, users, 300);
}
return users;
// Offset-based pagination (simple but slower for large offsets)
GET /users?page=2&limit=20
// Skip 20 records, return next 20
// Cursor-based pagination (better performance)
GET /users?cursor=eyJpZCI6MjB9&limit=20
// Start from cursor, return next 20
// Response includes next cursor
{
"data": [...],
"pagination": {
"nextCursor": "eyJpZCI6NDB9",
"hasMore": true
}
}
// Enable gzip compression
app.use(compression({
level: 6,
threshold: 1024,
filter: (req, res) => {
if (req.headers['x-no-compression']) {
return false;
}
return compression.filter(req, res);
}
}));
// Headers
Accept-Encoding: gzip, deflate
Content-Encoding: gzip
// For long-running operations, return immediately and process async
POST /api/v1/reports
Response: 202 Accepted
{
"jobId": "abc123",
"status": "processing",
"statusUrl": "/api/v1/jobs/abc123"
}
// Client polls status
GET /api/v1/jobs/abc123
Response: 200 OK
{
"jobId": "abc123",
"status": "completed",
"resultUrl": "/api/v1/reports/abc123"
}