What is a REST API?
REST (REpresentational State Transfer) is an architectural style for designing networked applications. REST APIs use HTTP requests to perform CRUD (Create, Read, Update, Delete) operations on resources.
🏗️ REST Principles
// 1. Client-Server Architecture
// Client and server are separate, communicate via HTTP
// 2. Stateless
// Each request contains all information needed
// Server doesn't store client state between requests
// 3. Cacheable
// Responses should define themselves as cacheable or not
// 4. Uniform Interface
// Standardized way to communicate (HTTP methods, URIs, etc.)
// 5. Layered System
// Client doesn't know if connected directly to server or through intermediary
// 6. Resource-Based
// Everything is a resource identified by URI
// Resources manipulated using representations (JSON, XML, etc.)
📍 HTTP Methods (CRUD)
// GET - Retrieve resource(s)
// Safe: doesn't modify data
// Idempotent: same result every time
fetch('/api/users') // Get all users
fetch('/api/users/123') // Get user with ID 123
fetch('/api/users/123/posts') // Get posts by user 123
// POST - Create new resource
// Not idempotent: creates new resource each time
fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: 'John',
email: 'john@example.com'
})
})
// PUT - Update/Replace entire resource
// Idempotent: same result if repeated
fetch('/api/users/123', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: 'John Doe',
email: 'john@example.com',
age: 30 // All fields required
})
})
// PATCH - Partial update
// Not necessarily idempotent
fetch('/api/users/123', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
age: 31 // Only update age
})
})
// DELETE - Remove resource
// Idempotent: same result if repeated
fetch('/api/users/123', {
method: 'DELETE'
})
// HEAD - Like GET but only headers (no body)
fetch('/api/users/123', { method: 'HEAD' })
// OPTIONS - Get supported methods
fetch('/api/users', { method: 'OPTIONS' })
🔢 HTTP Status Codes
// 2xx Success
200 // OK - Request succeeded
201 // Created - Resource created (POST)
202 // Accepted - Request accepted, processing
204 // No Content - Success but no response body (DELETE)
// 3xx Redirection
301 // Moved Permanently
302 // Found - Temporary redirect
304 // Not Modified - Cached version still valid
// 4xx Client Errors
400 // Bad Request - Invalid syntax/data
401 // Unauthorized - Authentication required
403 // Forbidden - Authenticated but not allowed
404 // Not Found - Resource doesn't exist
405 // Method Not Allowed - HTTP method not supported
409 // Conflict - Request conflicts with current state
422 // Unprocessable Entity - Validation errors
429 // Too Many Requests - Rate limit exceeded
// 5xx Server Errors
500 // Internal Server Error - Generic server error
502 // Bad Gateway - Invalid response from upstream
503 // Service Unavailable - Server temporarily down
504 // Gateway Timeout - Upstream timeout
// Check status code
let response = await fetch('/api/users');
if (response.status === 200) {
let data = await response.json();
}
if (response.status === 404) {
console.log('User not found');
}
if (response.status >= 500) {
console.log('Server error');
}
// Use response.ok for success
if (response.ok) { // 200-299
let data = await response.json();
} else {
console.error(`HTTP ${response.status}`);
}
🛤️ URL Design Patterns
// Resources as nouns (not verbs)
// ✅ Good
GET /api/users
POST /api/users
GET /api/users/123
PUT /api/users/123
DELETE /api/users/123
// ❌ Bad
GET /api/getAllUsers
POST /api/createUser
GET /api/getUserById/123
PUT /api/updateUser/123
DELETE /api/deleteUser/123
// Hierarchical relationships
GET /api/users/123/posts // All posts by user 123
GET /api/users/123/posts/456 // Post 456 by user 123
POST /api/users/123/posts // Create post for user 123
GET /api/posts/456/comments // Comments on post 456
// Query parameters for filtering/sorting/pagination
GET /api/users?role=admin
GET /api/users?page=2&limit=20
GET /api/users?sort=name&order=asc
GET /api/posts?author=123&status=published
// Versioning
GET /api/v1/users
GET /api/v2/users
// Or subdomain
GET https://api-v1.example.com/users
GET https://api-v2.example.com/users
// Plural nouns for collections
GET /api/users ✅
GET /api/user ❌
// Lowercase with hyphens
GET /api/user-profiles ✅
GET /api/userProfiles ❌
GET /api/user_profiles ❌
📨 Request/Response Format
Request Headers
fetch('/api/users', {
headers: {
// Content type of request body
'Content-Type': 'application/json',
// Accepted response format
'Accept': 'application/json',
// Authentication
'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
// API key
'X-API-Key': 'your-api-key',
// Custom headers (prefix with X-)
'X-Request-ID': 'abc-123',
'X-Client-Version': '1.2.3'
}
})
// Common Content-Type values
'application/json' // JSON data
'application/x-www-form-urlencoded' // Form data
'multipart/form-data' // File upload
'text/plain' // Plain text
'text/html' // HTML
Response Structure
// Success response
{
"data": {
"id": 123,
"name": "John Doe",
"email": "john@example.com"
},
"meta": {
"timestamp": "2024-01-15T10:30:00Z"
}
}
// Collection response with pagination
{
"data": [
{ "id": 1, "name": "User 1" },
{ "id": 2, "name": "User 2" }
],
"meta": {
"page": 1,
"perPage": 20,
"total": 100,
"pages": 5
},
"links": {
"self": "/api/users?page=1",
"first": "/api/users?page=1",
"prev": null,
"next": "/api/users?page=2",
"last": "/api/users?page=5"
}
}
// Error response
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid input data",
"details": [
{
"field": "email",
"message": "Invalid email format"
},
{
"field": "age",
"message": "Must be 18 or older"
}
]
},
"meta": {
"timestamp": "2024-01-15T10:30:00Z",
"requestId": "abc-123"
}
}
// Created response (201)
// Include Location header with new resource URL
{
"data": {
"id": 124,
"name": "New User",
"email": "new@example.com"
},
"meta": {
"location": "/api/users/124"
}
}
🔐 Authentication & Authorization
API Key
// In header
fetch('/api/users', {
headers: {
'X-API-Key': 'your-api-key-here'
}
})
// In query parameter
fetch('/api/users?api_key=your-api-key-here')
// Simple but less secure
// Use HTTPS to protect key in transit
Bearer Token (JWT)
// Login to get token
let response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: 'user@example.com',
password: 'password123'
})
});
let { token } = await response.json();
// Store token
localStorage.setItem('token', token);
// Use token in subsequent requests
fetch('/api/users', {
headers: {
'Authorization': `Bearer ${token}`
}
})
// API wrapper with auto-authentication
class API {
constructor(baseURL) {
this.baseURL = baseURL;
}
getToken() {
return localStorage.getItem('token');
}
async request(endpoint, options = {}) {
let token = this.getToken();
let headers = {
'Content-Type': 'application/json',
...options.headers
};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
let response = await fetch(`${this.baseURL}${endpoint}`, {
...options,
headers
});
if (response.status === 401) {
// Token expired, redirect to login
localStorage.removeItem('token');
window.location.href = '/login';
throw new Error('Unauthorized');
}
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
}
get(endpoint) {
return this.request(endpoint);
}
post(endpoint, data) {
return this.request(endpoint, {
method: 'POST',
body: JSON.stringify(data)
});
}
}
let api = new API('https://api.example.com');
let users = await api.get('/users');
OAuth 2.0
// OAuth flow (simplified)
// 1. Redirect user to authorization URL
let authUrl = 'https://provider.com/oauth/authorize?' +
'client_id=YOUR_CLIENT_ID&' +
'redirect_uri=https://yourapp.com/callback&' +
'response_type=code&' +
'scope=read write';
window.location.href = authUrl;
// 2. User approves, provider redirects back with code
// https://yourapp.com/callback?code=AUTH_CODE
// 3. Exchange code for access token
let params = new URLSearchParams(window.location.search);
let code = params.get('code');
let response = await fetch('https://provider.com/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
client_id: 'YOUR_CLIENT_ID',
client_secret: 'YOUR_CLIENT_SECRET',
redirect_uri: 'https://yourapp.com/callback'
})
});
let { access_token, refresh_token } = await response.json();
// 4. Use access token
fetch('/api/users', {
headers: {
'Authorization': `Bearer ${access_token}`
}
})
🔄 Pagination Patterns
// Offset-based pagination
GET /api/users?offset=0&limit=20 // First 20
GET /api/users?offset=20&limit=20 // Next 20
async function fetchPage(page, perPage = 20) {
let offset = (page - 1) * perPage;
let response = await fetch(`/api/users?offset=${offset}&limit=${perPage}`);
return response.json();
}
// Page-based pagination
GET /api/users?page=1&per_page=20
GET /api/users?page=2&per_page=20
async function* fetchAllPages(endpoint, perPage = 20) {
let page = 1;
let hasMore = true;
while (hasMore) {
let response = await fetch(`${endpoint}?page=${page}&per_page=${perPage}`);
let data = await response.json();
yield data.data;
hasMore = page < data.meta.pages;
page++;
}
}
// Usage
for await (let users of fetchAllPages('/api/users')) {
console.log('Page:', users);
}
// Cursor-based pagination (for large datasets)
GET /api/users?cursor=abc123&limit=20
let cursor = null;
let users = [];
while (true) {
let url = `/api/users?limit=20${cursor ? `&cursor=${cursor}` : ''}`;
let response = await fetch(url);
let data = await response.json();
users.push(...data.data);
if (!data.meta.nextCursor) break;
cursor = data.meta.nextCursor;
}
// Link header pagination (GitHub style)
// Response includes Link header:
// Link: ; rel="next",
// ; rel="last"
function parseLink(header) {
let links = {};
let parts = header.split(',');
parts.forEach(part => {
let [url, rel] = part.split(';');
let urlMatch = url.match(/<(.+)>/);
let relMatch = rel.match(/rel="(.+)"/);
if (urlMatch && relMatch) {
links[relMatch[1]] = urlMatch[1];
}
});
return links;
}
let response = await fetch('/api/users');
let linkHeader = response.headers.get('Link');
let links = parseLink(linkHeader);
if (links.next) {
let nextResponse = await fetch(links.next);
}
🔍 Filtering, Sorting, Searching
// Filtering
GET /api/users?role=admin
GET /api/users?status=active&role=admin
GET /api/posts?author=123&published=true
// Multiple values
GET /api/users?role=admin,moderator
GET /api/users?id=1,2,3,4,5
// Comparison operators
GET /api/users?age[gte]=18 // age >= 18
GET /api/users?age[lt]=65 // age < 65
GET /api/users?created[gte]=2024-01-01
// Sorting
GET /api/users?sort=name // Ascending
GET /api/users?sort=-name // Descending (-)
GET /api/users?sort=name,-created // Multiple fields
// Alternative syntax
GET /api/users?sort=name&order=asc
GET /api/users?sort=name&order=desc
// Searching
GET /api/users?search=john
GET /api/users?q=javascript
// Field selection (sparse fieldsets)
GET /api/users?fields=id,name,email
GET /api/users/123?fields=name,email
// Include related resources
GET /api/posts?include=author,comments
GET /api/users/123?include=posts,profile
// Combined example
async function fetchUsers(filters = {}) {
let params = new URLSearchParams();
if (filters.role) params.append('role', filters.role);
if (filters.status) params.append('status', filters.status);
if (filters.search) params.append('search', filters.search);
if (filters.sort) params.append('sort', filters.sort);
if (filters.page) params.append('page', filters.page);
if (filters.limit) params.append('limit', filters.limit);
let url = `/api/users?${params}`;
let response = await fetch(url);
return response.json();
}
// Usage
let users = await fetchUsers({
role: 'admin',
status: 'active',
sort: '-created',
page: 1,
limit: 20
});
⚠️ Error Handling
// Comprehensive error handling
async function apiRequest(url, options = {}) {
try {
let response = await fetch(url, options);
// Check HTTP status
if (!response.ok) {
let error = await response.json().catch(() => ({}));
switch (response.status) {
case 400:
throw new ValidationError(error.message, error.details);
case 401:
throw new AuthError('Unauthorized');
case 403:
throw new AuthError('Forbidden');
case 404:
throw new NotFoundError('Resource not found');
case 429:
throw new RateLimitError('Too many requests');
case 500:
throw new ServerError('Internal server error');
default:
throw new APIError(`HTTP ${response.status}`, response.status);
}
}
return response.json();
} catch (error) {
if (error instanceof TypeError) {
// Network error
throw new NetworkError('Network request failed');
}
throw error;
}
}
// Custom error classes
class APIError extends Error {
constructor(message, status) {
super(message);
this.name = 'APIError';
this.status = status;
}
}
class ValidationError extends APIError {
constructor(message, details) {
super(message, 400);
this.name = 'ValidationError';
this.details = details;
}
}
class AuthError extends APIError {
constructor(message) {
super(message, 401);
this.name = 'AuthError';
}
}
class NotFoundError extends APIError {
constructor(message) {
super(message, 404);
this.name = 'NotFoundError';
}
}
class RateLimitError extends APIError {
constructor(message) {
super(message, 429);
this.name = 'RateLimitError';
}
}
class ServerError extends APIError {
constructor(message) {
super(message, 500);
this.name = 'ServerError';
}
}
class NetworkError extends Error {
constructor(message) {
super(message);
this.name = 'NetworkError';
}
}
// Usage
try {
let user = await apiRequest('/api/users/123');
console.log(user);
} catch (error) {
if (error instanceof ValidationError) {
console.error('Validation errors:', error.details);
} else if (error instanceof AuthError) {
redirectToLogin();
} else if (error instanceof NotFoundError) {
show404Page();
} else if (error instanceof NetworkError) {
showOfflineMessage();
} else {
console.error('Unexpected error:', error);
}
}
💡 Practical Examples
Full CRUD Operations
class UserService {
constructor(baseURL) {
this.baseURL = baseURL;
}
async getAll(params = {}) {
let query = new URLSearchParams(params);
let response = await fetch(`${this.baseURL}/users?${query}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
}
async getById(id) {
let response = await fetch(`${this.baseURL}/users/${id}`);
if (response.status === 404) {
return null;
}
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
}
async create(userData) {
let response = await fetch(`${this.baseURL}/users`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData)
});
if (!response.ok) {
let error = await response.json();
throw new Error(error.message);
}
return response.json();
}
async update(id, userData) {
let response = await fetch(`${this.baseURL}/users/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData)
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
}
async patch(id, updates) {
let response = await fetch(`${this.baseURL}/users/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updates)
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
}
async delete(id) {
let response = await fetch(`${this.baseURL}/users/${id}`, {
method: 'DELETE'
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.status === 204 ? null : response.json();
}
}
// Usage
let users = new UserService('https://api.example.com');
let allUsers = await users.getAll({ page: 1, limit: 20 });
let user = await users.getById(123);
let newUser = await users.create({ name: 'John', email: 'john@example.com' });
let updated = await users.update(123, { name: 'John Doe', email: 'john@example.com' });
let patched = await users.patch(123, { email: 'newemail@example.com' });
await users.delete(123);
Rate Limiting
class RateLimiter {
constructor(maxRequests, windowMs) {
this.maxRequests = maxRequests;
this.windowMs = windowMs;
this.requests = [];
}
async throttle() {
let now = Date.now();
// Remove old requests outside window
this.requests = this.requests.filter(
time => now - time < this.windowMs
);
if (this.requests.length >= this.maxRequests) {
let oldestRequest = this.requests[0];
let waitTime = this.windowMs - (now - oldestRequest);
console.log(`Rate limited. Waiting ${waitTime}ms`);
await new Promise(resolve => setTimeout(resolve, waitTime));
return this.throttle();
}
this.requests.push(now);
}
}
// Usage
let limiter = new RateLimiter(10, 60000); // 10 requests per minute
async function fetchWithLimit(url) {
await limiter.throttle();
return fetch(url);
}
Batch Requests
// Batch API pattern
async function batchFetch(ids) {
// Single request for multiple resources
let idsParam = ids.join(',');
let response = await fetch(`/api/users?ids=${idsParam}`);
return response.json();
}
// Or using POST
async function batchFetch(ids) {
let response = await fetch('/api/users/batch', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ids })
});
return response.json();
}
// DataLoader pattern (automatic batching)
class DataLoader {
constructor(batchFn, maxBatchSize = 100) {
this.batchFn = batchFn;
this.maxBatchSize = maxBatchSize;
this.queue = [];
this.cache = new Map();
}
load(key) {
if (this.cache.has(key)) {
return Promise.resolve(this.cache.get(key));
}
return new Promise((resolve, reject) => {
this.queue.push({ key, resolve, reject });
if (this.queue.length === 1) {
process.nextTick(() => this.dispatch());
}
});
}
async dispatch() {
let queue = this.queue.slice();
this.queue = [];
let keys = queue.map(item => item.key);
try {
let results = await this.batchFn(keys);
queue.forEach((item, index) => {
let result = results[index];
this.cache.set(item.key, result);
item.resolve(result);
});
} catch (error) {
queue.forEach(item => item.reject(error));
}
}
}
// Usage
let userLoader = new DataLoader(async ids => {
let response = await fetch(`/api/users?ids=${ids.join(',')}`);
return response.json();
});
// These will be batched automatically
let user1Promise = userLoader.load(1);
let user2Promise = userLoader.load(2);
let user3Promise = userLoader.load(3);
let [user1, user2, user3] = await Promise.all([
user1Promise,
user2Promise,
user3Promise
]);
// Single request: GET /api/users?ids=1,2,3
🎯 Key Takeaways
- REST Principles: Stateless, resource-based, uniform interface
- HTTP Methods: GET (read), POST (create), PUT (replace), PATCH (update), DELETE (remove)
- Status Codes: 2xx success, 3xx redirect, 4xx client error, 5xx server error
- URL Design: Use nouns, plural, hierarchical relationships
- Authentication: API keys, Bearer tokens (JWT), OAuth 2.0
- Pagination: Offset, page-based, cursor-based for large datasets
- Filtering: Query params for filter, sort, search, field selection
- Error Handling: Proper status codes, detailed error messages, custom error classes