API design is the foundation of modern software architecture. Whether you're building microservices, mobile applications, or integrating third-party services, well-designed APIs determine the success of your software ecosystem. This comprehensive guide covers everything developers need to know about API design and architecture, from fundamental principles to advanced patterns.
Key Takeaways:
- Learn proven API design principles that scale
- Master REST, GraphQL, and modern API patterns
- Implement security, versioning, and documentation best practices
- Build APIs that developers love to use
What is API Design?
API design is the process of creating application programming interfaces that are intuitive, maintainable, and scalable. It involves defining endpoints, request/response formats, authentication mechanisms, and error handling strategies before writing implementation code.
Why API Design Matters
Good API design delivers:
- Developer Experience (DX): Intuitive interfaces that reduce integration time
- Maintainability: Clear contracts that evolve without breaking changes
- Performance: Efficient data transfer and minimal over-fetching
- Security: Robust authentication and authorization mechanisms
- Scalability: Architecture that handles growth gracefully
Design-First vs Code-First Approach
Design-First (Recommended):
# OpenAPI 3.0 Specification Example
openapi: 3.0.0
info:
title: User Management API
version: 1.0.0
description: API for managing user resources
paths:
/api/v1/users:
get:
summary: Retrieve 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: object
properties:
data:
type: array
items:
$ref: "#/components/schemas/User"
pagination:
$ref: "#/components/schemas/Pagination"
components:
schemas:
User:
type: object
required:
- id
- email
- username
properties:
id:
type: string
format: uuid
email:
type: string
format: email
username:
type: string
createdAt:
type: string
format: date-time
Benefits of Design-First:
- Early validation and stakeholder feedback
- Automatic documentation generation
- Mock server creation before implementation
- Contract testing between frontend and backend teams
API Design Principles
1. Consistency
Maintain consistent naming conventions, response structures, and error formats across all endpoints.
// Good: Consistent naming and structure
GET /api/v1/users
POST /api/v1/users
GET /api/v1/users/{id}
PUT /api/v1/users/{id}
DELETE /api/v1/users/{id}
GET /api/v1/products
POST /api/v1/products
GET /api/v1/products/{id}
PUT /api/v1/products/{id}
DELETE /api/v1/products/{id}
// Bad: Inconsistent naming
GET /api/v1/users
POST /api/v1/createUser
GET /api/v1/user/{id}
PUT /api/v1/updateUser/{id}
DELETE /api/v1/user/delete/{id}
2. Simplicity and Intuitive Design
APIs should be self-explanatory. Developers should understand endpoints without extensive documentation.
// Express.js Example: Simple, Intuitive Routes
const express = require("express");
const router = express.Router();
// Clear, self-documenting endpoints
router.get("/users", async (req, res) => {
const { page = 1, limit = 20, search = "" } = req.query;
try {
const users = await User.find({
username: new RegExp(search, "i"),
})
.limit(limit)
.skip((page - 1) * limit)
.select("-password");
const total = await User.countDocuments();
res.json({
data: users,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
pages: Math.ceil(total / limit),
},
});
} catch (error) {
res.status(500).json({
error: "Internal server error",
message: error.message,
});
}
});
module.exports = router;
3. Resource-Oriented Design
Design APIs around resources (nouns) rather than actions (verbs).
# FastAPI Example: Resource-Oriented Design
from fastapi import FastAPI, HTTPException, Query
from pydantic import BaseModel, EmailStr
from typing import List, Optional
from datetime import datetime
from uuid import UUID, uuid4
app = FastAPI()
class User(BaseModel):
id: UUID
email: EmailStr
username: str
created_at: datetime
class UserCreate(BaseModel):
email: EmailStr
username: str
password: str
class UserUpdate(BaseModel):
email: Optional[EmailStr] = None
username: Optional[str] = None
# Resource: Users
@app.get("/api/v1/users", response_model=List[User])
async def list_users(
page: int = Query(1, ge=1),
limit: int = Query(20, ge=1, le=100)
):
"""Retrieve paginated list of users"""
# Implementation
return users
@app.post("/api/v1/users", response_model=User, status_code=201)
async def create_user(user: UserCreate):
"""Create a new user"""
# Implementation
return new_user
@app.get("/api/v1/users/{user_id}", response_model=User)
async def get_user(user_id: UUID):
"""Retrieve a specific user by ID"""
# Implementation
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user
@app.put("/api/v1/users/{user_id}", response_model=User)
async def update_user(user_id: UUID, user_update: UserUpdate):
"""Update a user's information"""
# Implementation
return updated_user
@app.delete("/api/v1/users/{user_id}", status_code=204)
async def delete_user(user_id: UUID):
"""Delete a user"""
# Implementation
return None
4. Proper HTTP Method Usage
Use HTTP methods according to their semantic meaning:
Method | Purpose | Idempotent | Safe |
---|---|---|---|
GET | Retrieve resources | Yes | Yes |
POST | Create new resources | No | No |
PUT | Replace entire resource | Yes | No |
PATCH | Partial update | No | No |
DELETE | Remove resource | Yes | No |
// Go (Gin Framework) Example: Proper HTTP Methods
package main
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type User struct {
ID string `json:"id"`
Email string `json:"email" binding:"required,email"`
Username string `json:"username" binding:"required"`
CreatedAt time.Time `json:"created_at"`
}
func main() {
router := gin.Default()
v1 := router.Group("/api/v1")
{
users := v1.Group("/users")
{
// Safe, Idempotent - retrieves without side effects
users.GET("", listUsers)
users.GET("/:id", getUser)
// Non-idempotent - creates new resource each time
users.POST("", createUser)
// Idempotent - replaces entire resource
users.PUT("/:id", replaceUser)
// Partial update
users.PATCH("/:id", updateUser)
// Idempotent - multiple deletions have same effect
users.DELETE("/:id", deleteUser)
}
}
router.Run(":8080")
}
func createUser(c *gin.Context) {
var user User
if err := c.ShouldBindJSON(&user); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Validation failed",
"details": err.Error(),
})
return
}
user.ID = uuid.New().String()
user.CreatedAt = time.Now()
// Save to database
c.JSON(http.StatusCreated, user)
}
func updateUser(c *gin.Context) {
id := c.Param("id")
var updates map[string]interface{}
if err := c.ShouldBindJSON(&updates); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Apply partial updates
c.JSON(http.StatusOK, updatedUser)
}
5. Meaningful Status Codes
Return appropriate HTTP status codes that clearly communicate the result.
// TypeScript (Express) Example: Comprehensive Status Codes
import express, { Request, Response, NextFunction } from "express";
const app = express();
// Success Responses
app.get("/api/v1/users", async (req: Request, res: Response) => {
const users = await fetchUsers();
res.status(200).json({ data: users }); // 200 OK
});
app.post("/api/v1/users", async (req: Request, res: Response) => {
const user = await createUser(req.body);
res.status(201).json({ data: user }); // 201 Created
});
app.delete("/api/v1/users/:id", async (req: Request, res: Response) => {
await deleteUser(req.params.id);
res.status(204).send(); // 204 No Content
});
// Client Error Responses
app.get("/api/v1/users/:id", async (req: Request, res: Response) => {
const user = await findUser(req.params.id);
if (!user) {
return res.status(404).json({
// 404 Not Found
error: "User not found",
code: "USER_NOT_FOUND",
});
}
res.status(200).json({ data: user });
});
app.post("/api/v1/users", async (req: Request, res: Response) => {
const { email, username, password } = req.body;
// Validation error
if (!email || !username || !password) {
return res.status(400).json({
// 400 Bad Request
error: "Validation failed",
code: "VALIDATION_ERROR",
details: {
email: !email ? "Email is required" : null,
username: !username ? "Username is required" : null,
password: !password ? "Password is required" : null,
},
});
}
// Conflict - user already exists
const existingUser = await findUserByEmail(email);
if (existingUser) {
return res.status(409).json({
// 409 Conflict
error: "User with this email already exists",
code: "USER_EXISTS",
});
}
const user = await createUser({ email, username, password });
res.status(201).json({ data: user });
});
// Authorization and Authentication
app.get(
"/api/v1/admin/users",
authenticateToken,
async (req: Request, res: Response) => {
if (!req.user) {
return res.status(401).json({
// 401 Unauthorized
error: "Authentication required",
code: "AUTH_REQUIRED",
});
}
if (!req.user.isAdmin) {
return res.status(403).json({
// 403 Forbidden
error: "Insufficient permissions",
code: "FORBIDDEN",
});
}
const users = await fetchAllUsers();
res.status(200).json({ data: users });
}
);
// Server Errors
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
console.error(err.stack);
res.status(500).json({
// 500 Internal Server Error
error: "An unexpected error occurred",
code: "INTERNAL_ERROR",
...(process.env.NODE_ENV === "development" && {
message: err.message,
stack: err.stack,
}),
});
});
// Rate Limiting
app.use((req: Request, res: Response, next: NextFunction) => {
if (rateLimitExceeded(req)) {
return res.status(429).json({
// 429 Too Many Requests
error: "Rate limit exceeded",
code: "RATE_LIMIT_EXCEEDED",
retryAfter: 60,
});
}
next();
});
Common HTTP Status Codes Reference
2xx Success:
200 OK
- Request succeeded201 Created
- Resource created successfully204 No Content
- Success with no response body
4xx Client Errors:
400 Bad Request
- Invalid request format401 Unauthorized
- Authentication required403 Forbidden
- Insufficient permissions404 Not Found
- Resource doesn't exist409 Conflict
- Resource conflict (e.g., duplicate)422 Unprocessable Entity
- Validation errors429 Too Many Requests
- Rate limit exceeded
5xx Server Errors:
500 Internal Server Error
- Unexpected server error502 Bad Gateway
- Invalid upstream response503 Service Unavailable
- Server temporarily unavailable
API Architecture Patterns
1. RESTful Architecture
REST (Representational State Transfer) remains the most popular API architecture pattern.
REST Constraints:
- Client-Server separation
- Stateless communication
- Cacheable responses
- Uniform interface
- Layered system
- Code on demand (optional)
// Spring Boot REST API Example
@RestController
@RequestMapping("/api/v1/products")
@Validated
public class ProductController {
@Autowired
private ProductService productService;
@GetMapping
public ResponseEntity<PagedResponse<ProductDTO>> getProducts(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(required = false) String category,
@RequestParam(required = false) BigDecimal minPrice,
@RequestParam(required = false) BigDecimal maxPrice
) {
ProductFilterDTO filter = ProductFilterDTO.builder()
.category(category)
.minPrice(minPrice)
.maxPrice(maxPrice)
.build();
Page<Product> products = productService.findAll(filter,
PageRequest.of(page, size, Sort.by("createdAt").descending()));
List<ProductDTO> productDTOs = products.getContent()
.stream()
.map(this::convertToDTO)
.collect(Collectors.toList());
PagedResponse<ProductDTO> response = new PagedResponse<>(
productDTOs,
products.getNumber(),
products.getSize(),
products.getTotalElements(),
products.getTotalPages(),
products.isLast()
);
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(5, TimeUnit.MINUTES))
.body(response);
}
@GetMapping("/{id}")
public ResponseEntity<ProductDTO> getProduct(@PathVariable UUID id) {
Product product = productService.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Product not found"));
return ResponseEntity.ok()
.eTag(String.valueOf(product.getVersion()))
.body(convertToDTO(product));
}
@PostMapping
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<ProductDTO> createProduct(
@Valid @RequestBody ProductCreateDTO productDTO
) {
Product product = productService.create(convertToEntity(productDTO));
URI location = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(product.getId())
.toUri();
return ResponseEntity.created(location)
.body(convertToDTO(product));
}
@PutMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<ProductDTO> updateProduct(
@PathVariable UUID id,
@Valid @RequestBody ProductUpdateDTO productDTO,
@RequestHeader("If-Match") String ifMatch
) {
Product product = productService.update(id, productDTO, ifMatch);
return ResponseEntity.ok()
.eTag(String.valueOf(product.getVersion()))
.body(convertToDTO(product));
}
@DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<Void> deleteProduct(@PathVariable UUID id) {
productService.delete(id);
return ResponseEntity.noContent().build();
}
private ProductDTO convertToDTO(Product product) {
return modelMapper.map(product, ProductDTO.class);
}
private Product convertToEntity(ProductCreateDTO dto) {
return modelMapper.map(dto, Product.class);
}
}
2. GraphQL Architecture
GraphQL offers flexible querying, allowing clients to request exactly the data they need.
// GraphQL Schema Definition
const { ApolloServer, gql } = require("apollo-server-express");
const typeDefs = gql`
type User {
id: ID!
email: String!
username: String!
posts: [Post!]!
followers: [User!]!
following: [User!]!
createdAt: DateTime!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
comments: [Comment!]!
likes: Int!
createdAt: DateTime!
updatedAt: DateTime!
}
type Comment {
id: ID!
content: String!
author: User!
post: Post!
createdAt: DateTime!
}
type Query {
user(id: ID!): User
users(limit: Int = 10, offset: Int = 0): [User!]!
post(id: ID!): Post
posts(authorId: ID, limit: Int = 10, offset: Int = 0): [Post!]!
searchPosts(query: String!): [Post!]!
}
type Mutation {
createUser(email: String!, username: String!, password: String!): User!
updateUser(id: ID!, email: String, username: String): User!
deleteUser(id: ID!): Boolean!
createPost(title: String!, content: String!): Post!
updatePost(id: ID!, title: String, content: String): Post!
deletePost(id: ID!): Boolean!
createComment(postId: ID!, content: String!): Comment!
likePost(postId: ID!): Post!
}
type Subscription {
postCreated(authorId: ID): Post!
commentAdded(postId: ID!): Comment!
}
scalar DateTime
`;
const resolvers = {
Query: {
user: async (_, { id }, { dataSources }) => {
return await dataSources.userAPI.getUserById(id);
},
posts: async (_, { authorId, limit, offset }, { dataSources }) => {
return await dataSources.postAPI.getPosts({ authorId, limit, offset });
},
searchPosts: async (_, { query }, { dataSources }) => {
return await dataSources.postAPI.searchPosts(query);
},
},
Mutation: {
createUser: async (_, { email, username, password }, { dataSources }) => {
return await dataSources.userAPI.createUser({
email,
username,
password,
});
},
createPost: async (_, { title, content }, { user, dataSources }) => {
if (!user) throw new Error("Authentication required");
return await dataSources.postAPI.createPost({
title,
content,
authorId: user.id,
});
},
likePost: async (_, { postId }, { user, dataSources }) => {
if (!user) throw new Error("Authentication required");
return await dataSources.postAPI.likePost(postId, user.id);
},
},
User: {
posts: async (user, _, { dataSources }) => {
return await dataSources.postAPI.getPostsByAuthor(user.id);
},
followers: async (user, _, { dataSources }) => {
return await dataSources.userAPI.getFollowers(user.id);
},
},
Post: {
author: async (post, _, { dataSources }) => {
return await dataSources.userAPI.getUserById(post.authorId);
},
comments: async (post, _, { dataSources }) => {
return await dataSources.commentAPI.getCommentsByPost(post.id);
},
},
};
// DataLoader for N+1 problem prevention
const DataLoader = require("dataloader");
class UserAPI {
constructor() {
this.loader = new DataLoader(async (ids) => {
const users = await User.find({ _id: { $in: ids } });
return ids.map((id) => users.find((user) => user.id === id));
});
}
async getUserById(id) {
return this.loader.load(id);
}
}
3. Event-Driven Architecture
For real-time updates and asynchronous processing.
# Python (FastAPI + WebSockets) Event-Driven API
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from typing import List, Dict
import asyncio
import json
from datetime import datetime
app = FastAPI()
class ConnectionManager:
def __init__(self):
self.active_connections: Dict[str, List[WebSocket]] = {}
async def connect(self, websocket: WebSocket, channel: str):
await websocket.accept()
if channel not in self.active_connections:
self.active_connections[channel] = []
self.active_connections[channel].append(websocket)
def disconnect(self, websocket: WebSocket, channel: str):
self.active_connections[channel].remove(websocket)
async def broadcast(self, message: dict, channel: str):
if channel in self.active_connections:
for connection in self.active_connections[channel]:
await connection.send_json(message)
manager = ConnectionManager()
@app.websocket("/ws/notifications/{user_id}")
async def websocket_endpoint(websocket: WebSocket, user_id: str):
channel = f"user:{user_id}"
await manager.connect(websocket, channel)
try:
while True:
data = await websocket.receive_text()
# Process incoming messages if needed
except WebSocketDisconnect:
manager.disconnect(websocket, channel)
@app.post("/api/v1/events/publish")
async def publish_event(event: dict):
"""Publish event to specific channel"""
channel = event.get('channel')
message = {
'type': event.get('type'),
'data': event.get('data'),
'timestamp': datetime.utcnow().isoformat()
}
await manager.broadcast(message, channel)
return {'status': 'published'}
# Message Queue Integration (RabbitMQ/Redis)
import aio_pika
import asyncio
async def process_events():
connection = await aio_pika.connect_robust("amqp://guest:guest@localhost/")
async with connection:
channel = await connection.channel()
queue = await channel.declare_queue('events', durable=True)
async with queue.iterator() as queue_iter:
async for message in queue_iter:
async with message.process():
event_data = json.loads(message.body)
# Broadcast to WebSocket clients
await manager.broadcast(
event_data,
event_data.get('channel')
)
RESTful API Design Best Practices
1. Pagination
Always paginate list endpoints to prevent performance issues.
# Ruby on Rails API Pagination Example
class Api::V1::UsersController < ApplicationController
def index
page = params[:page]&.to_i || 1
per_page = [params[:per_page]&.to_i || 20, 100].min
users = User.page(page).per(per_page)
render json: {
data: users.map { |user| UserSerializer.new(user).as_json },
pagination: {
current_page: users.current_page,
per_page: users.limit_value,
total_pages: users.total_pages,
total_count: users.total_count,
next_page: users.next_page,
prev_page: users.prev_page
},
links: {
self: api_v1_users_url(page: page, per_page: per_page),
first: api_v1_users_url(page: 1, per_page: per_page),
last: api_v1_users_url(page: users.total_pages, per_page: per_page),
next: users.next_page ? api_v1_users_url(page: users.next_page, per_page: per_page) : nil,
prev: users.prev_page ? api_v1_users_url(page: users.prev_page, per_page: per_page) : nil
}
}
end
end
2. Filtering, Sorting, and Searching
// Node.js Advanced Query Builder
const express = require("express");
const router = express.Router();
router.get("/api/v1/products", async (req, res) => {
const {
// Filtering
category,
minPrice,
maxPrice,
inStock,
// Searching
search,
// Sorting
sortBy = "createdAt",
order = "desc",
// Pagination
page = 1,
limit = 20,
} = req.query;
// Build query
let query = Product.find();
// Apply filters
if (category) {
query = query.where("category").equals(category);
}
if (minPrice || maxPrice) {
query = query.where("price");
if (minPrice) query = query.gte(parseFloat(minPrice));
if (maxPrice) query = query.lte(parseFloat(maxPrice));
}
if (inStock !== undefined) {
query = query.where("stock").gt(0);
}
// Apply search
if (search) {
query = query.where({
$or: [
{ name: new RegExp(search, "i") },
{ description: new RegExp(search, "i") },
{ sku: new RegExp(search, "i") },
],
});
}
// Apply sorting
const sortOrder = order === "desc" ? "-" : "";
query = query.sort(`${sortOrder}${sortBy}`);
// Apply pagination
const skip = (page - 1) * limit;
query = query.skip(skip).limit(parseInt(limit));
// Execute query
const products = await query.exec();
const total = await Product.countDocuments(query.getFilter());
res.json({
data: products,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
pages: Math.ceil(total / limit),
},
filters: {
category,
minPrice,
maxPrice,
inStock,
search,
},
});
});
module.exports = router;
3. HATEOAS (Hypermedia as the Engine of Application State)
Include links to related resources in responses.
// C# ASP.NET Core HATEOAS Implementation
[ApiController]
[Route("api/v1/[controller]")]
public class OrdersController : ControllerBase
{
[HttpGet("{id}")]
public async Task<ActionResult<OrderResponse>> GetOrder(Guid id)
{
var order = await _orderService.GetOrderAsync(id);
if (order == null)
return NotFound();
var response = new OrderResponse
{
Id = order.Id,
CustomerId = order.CustomerId,
Total = order.Total,
Status = order.Status,
CreatedAt = order.CreatedAt,
// HATEOAS Links
Links = new List<Link>
{
new Link(
Url.Action(nameof(GetOrder), new { id = order.Id }),
"self",
"GET"
),
new Link(
Url.Action(nameof(UpdateOrder), new { id = order.Id }),
"update",
"PUT"
),
new Link(
Url.Action("GetCustomer", "Customers", new { id = order.CustomerId }),
"customer",
"GET"
),
new Link(
Url.Action("GetOrderItems", new { orderId = order.Id }),
"items",
"GET"
)
}
};
// Conditional links based on order status
if (order.Status == OrderStatus.Pending)
{
response.Links.Add(new Link(
Url.Action(nameof(CancelOrder), new { id = order.Id }),
"cancel",
"POST"
));
}
if (order.Status == OrderStatus.Paid)
{
response.Links.Add(new Link(
Url.Action(nameof(ShipOrder), new { id = order.Id }),
"ship",
"POST"
));
}
return Ok(response);
}
}
public class Link
{
public string Href { get; set; }
public string Rel { get; set; }
public string Method { get; set; }
public Link(string href, string rel, string method)
{
Href = href;
Rel = rel;
Method = method;
}
}
API Versioning Strategies
1. URI Versioning (Recommended)
GET /api/v1/users
GET /api/v2/users
2. Header Versioning
GET /api/users
Accept: application/vnd.myapi.v1+json
3. Query Parameter Versioning
GET /api/users?version=1
Implementation Example:
// PHP Laravel API Versioning
// routes/api.php
Route::prefix('v1')->group(function () {
Route::apiResource('users', 'Api\V1\UserController');
Route::apiResource('products', 'Api\V1\ProductController');
});
Route::prefix('v2')->group(function () {
Route::apiResource('users', 'Api\V2\UserController');
Route::apiResource('products', 'Api\V2\ProductController');
});
// app/Http/Controllers/Api/V1/UserController.php
namespace App\Http\Controllers\Api\V1;
class UserController extends Controller
{
public function index()
{
// V1 implementation
return response()->json([
'data' => User::all(),
'version' => '1.0'
]);
}
}
// app/Http/Controllers/Api/V2/UserController.php
namespace App\Http\Controllers\Api\V2;
class UserController extends Controller
{
public function index()
{
// V2 implementation with enhanced features
return response()->json([
'data' => User::with('profile')->get(),
'version' => '2.0',
'features' => ['includes_profile', 'enhanced_pagination']
]);
}
}
API Security and Authentication
1. JWT Authentication
// JWT Authentication Middleware (Node.js)
const jwt = require("jsonwebtoken");
const { promisify } = require("util");
const signToken = (payload) => {
return jwt.sign(payload, process.env.JWT_SECRET, {
expiresIn: process.env.JWT_EXPIRES_IN || "1h",
});
};
const verifyToken = promisify(jwt.verify);
// Authentication Middleware
const authenticate = async (req, res, next) => {
try {
// 1. Get token from header
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return res.status(401).json({
error: "No token provided",
code: "NO_TOKEN",
});
}
const token = authHeader.split(" ")[1];
// 2. Verify token
const decoded = await verifyToken(token, process.env.JWT_SECRET);
// 3. Check if user still exists
const user = await User.findById(decoded.id);
if (!user) {
return res.status(401).json({
error: "User no longer exists",
code: "USER_NOT_FOUND",
});
}
// 4. Check if user changed password after token was issued
if (user.passwordChangedAfter(decoded.iat)) {
return res.status(401).json({
error: "Password recently changed. Please log in again.",
code: "PASSWORD_CHANGED",
});
}
// 5. Grant access
req.user = user;
next();
} catch (error) {
if (error.name === "JsonWebTokenError") {
return res.status(401).json({
error: "Invalid token",
code: "INVALID_TOKEN",
});
}
if (error.name === "TokenExpiredError") {
return res.status(401).json({
error: "Token expired",
code: "TOKEN_EXPIRED",
});
}
res.status(500).json({
error: "Authentication failed",
code: "AUTH_ERROR",
});
}
};
// Login endpoint
router.post("/auth/login", async (req, res) => {
const { email, password } = req.body;
// 1. Check if email and password exist
if (!email || !password) {
return res.status(400).json({
error: "Please provide email and password",
code: "MISSING_CREDENTIALS",
});
}
// 2. Check if user exists && password is correct
const user = await User.findOne({ email }).select("+password");
if (!user || !(await user.correctPassword(password))) {
return res.status(401).json({
error: "Incorrect email or password",
code: "INVALID_CREDENTIALS",
});
}
// 3. Generate token
const token = signToken({ id: user._id });
res.json({
token,
user: {
id: user._id,
email: user.email,
username: user.username,
},
});
});
module.exports = { authenticate, signToken };
2. OAuth 2.0 Implementation
# Python FastAPI OAuth2 Implementation
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import JWTError, jwt
from passlib.context import CryptContext
from datetime import datetime, timedelta
from typing import Optional
app = FastAPI()
# Configuration
SECRET_KEY = "your-secret-key-here"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
class TokenData:
username: Optional[str] = None
def verify_password(plain_password, hashed_password):
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password):
return pwd_context.hash(password)
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
async def get_current_user(token: str = Depends(oauth2_scheme)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
token_data = TokenData(username=username)
except JWTError:
raise credentials_exception
user = get_user(username=token_data.username)
if user is None:
raise credentials_exception
return user
@app.post("/token")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
user = authenticate_user(form_data.username, form_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user.username},
expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer"}
@app.get("/api/v1/users/me")
async def read_users_me(current_user: User = Depends(get_current_user)):
return current_user
3. API Rate Limiting
// Go Rate Limiting with Redis
package middleware
import (
"context"
"fmt"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/go-redis/redis/v8"
)
type RateLimiter struct {
client *redis.Client
ctx context.Context
}
func NewRateLimiter(redisClient *redis.Client) *RateLimiter {
return &RateLimiter{
client: redisClient,
ctx: context.Background(),
}
}
// Sliding Window Rate Limiter
func (rl *RateLimiter) Limit(requestsPerWindow int, window time.Duration) gin.HandlerFunc {
return func(c *gin.Context) {
// Get identifier (IP or user ID)
identifier := getIdentifier(c)
key := fmt.Sprintf("rate_limit:%s", identifier)
// Current timestamp
now := time.Now().UnixNano()
windowStart := now - int64(window)
// Remove old entries
rl.client.ZRemRangeByScore(rl.ctx, key, "0", fmt.Sprintf("%d", windowStart))
// Count current requests
count, err := rl.client.ZCard(rl.ctx, key).Result()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Rate limit check failed",
})
c.Abort()
return
}
// Check if limit exceeded
if count >= int64(requestsPerWindow) {
c.Header("X-RateLimit-Limit", fmt.Sprintf("%d", requestsPerWindow))
c.Header("X-RateLimit-Remaining", "0")
// Get oldest request time to calculate retry-after
oldest, _ := rl.client.ZRange(rl.ctx, key, 0, 0).Result()
if len(oldest) > 0 {
retryAfter := window - time.Duration(now-parseInt64(oldest[0]))
c.Header("X-RateLimit-Reset", fmt.Sprintf("%d", time.Now().Add(retryAfter).Unix()))
c.Header("Retry-After", fmt.Sprintf("%d", int(retryAfter.Seconds())))
}
c.JSON(http.StatusTooManyRequests, gin.H{
"error": "Rate limit exceeded",
"code": "RATE_LIMIT_EXCEEDED",
})
c.Abort()
return
}
// Add current request
rl.client.ZAdd(rl.ctx, key, &redis.Z{
Score: float64(now),
Member: fmt.Sprintf("%d", now),
})
// Set expiry
rl.client.Expire(rl.ctx, key, window)
// Set rate limit headers
c.Header("X-RateLimit-Limit", fmt.Sprintf("%d", requestsPerWindow))
c.Header("X-RateLimit-Remaining", fmt.Sprintf("%d", requestsPerWindow-int(count)-1))
c.Header("X-RateLimit-Reset", fmt.Sprintf("%d", time.Now().Add(window).Unix()))
c.Next()
}
}
func getIdentifier(c *gin.Context) string {
// Try to get user ID from context (if authenticated)
if userID, exists := c.Get("userID"); exists {
return fmt.Sprintf("user:%v", userID)
}
// Fall back to IP address
return fmt.Sprintf("ip:%s", c.ClientIP())
}
API Documentation and Standards
OpenAPI/Swagger Documentation
# Complete OpenAPI 3.0 Specification
openapi: 3.0.3
info:
title: E-Commerce API
version: 1.0.0
description: |
RESTful API for e-commerce platform.
## Authentication
All endpoints require JWT authentication via Bearer token in Authorization header.
## Rate Limiting
- 100 requests per minute for authenticated users
- 20 requests per minute for unauthenticated requests
contact:
name: API Support
email: api@example.com
url: https://api.example.com/support
license:
name: MIT
url: https://opensource.org/licenses/MIT
servers:
- url: https://api.example.com/v1
description: Production server
- url: https://staging-api.example.com/v1
description: Staging server
- url: http://localhost:3000/v1
description: Development server
tags:
- name: Authentication
description: User authentication endpoints
- name: Users
description: User management
- name: Products
description: Product catalog
- name: Orders
description: Order management
paths:
/auth/login:
post:
summary: User login
tags: [Authentication]
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- email
- password
properties:
email:
type: string
format: email
example: user@example.com
password:
type: string
format: password
example: SecurePass123!
responses:
"200":
description: Login successful
content:
application/json:
schema:
type: object
properties:
token:
type: string
example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
user:
$ref: "#/components/schemas/User"
"401":
$ref: "#/components/responses/UnauthorizedError"
/products:
get:
summary: List products
tags: [Products]
parameters:
- $ref: "#/components/parameters/PageParam"
- $ref: "#/components/parameters/LimitParam"
- name: category
in: query
schema:
type: string
- name: minPrice
in: query
schema:
type: number
- name: maxPrice
in: query
schema:
type: number
responses:
"200":
description: Success
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: "#/components/schemas/Product"
pagination:
$ref: "#/components/schemas/Pagination"
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
schemas:
User:
type: object
properties:
id:
type: string
format: uuid
email:
type: string
format: email
username:
type: string
createdAt:
type: string
format: date-time
Product:
type: object
required:
- name
- price
properties:
id:
type: string
format: uuid
name:
type: string
description:
type: string
price:
type: number
format: decimal
category:
type: string
stock:
type: integer
createdAt:
type: string
format: date-time
Pagination:
type: object
properties:
page:
type: integer
limit:
type: integer
total:
type: integer
pages:
type: integer
Error:
type: object
properties:
error:
type: string
code:
type: string
details:
type: object
parameters:
PageParam:
name: page
in: query
schema:
type: integer
default: 1
minimum: 1
LimitParam:
name: limit
in: query
schema:
type: integer
default: 20
minimum: 1
maximum: 100
responses:
UnauthorizedError:
description: Authentication required
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
security:
- BearerAuth: []
Testing and Monitoring APIs
1. Unit Testing
// Jest API Testing Example
const request = require("supertest");
const app = require("../app");
const User = require("../models/User");
describe("User API Endpoints", () => {
let authToken;
let userId;
beforeAll(async () => {
// Setup test database
await setupTestDatabase();
});
afterAll(async () => {
// Cleanup
await cleanupTestDatabase();
});
describe("POST /api/v1/users", () => {
it("should create a new user", async () => {
const userData = {
email: "test@example.com",
username: "testuser",
password: "SecurePass123!",
};
const response = await request(app)
.post("/api/v1/users")
.send(userData)
.expect("Content-Type", /json/)
.expect(201);
expect(response.body.data).toHaveProperty("id");
expect(response.body.data.email).toBe(userData.email);
expect(response.body.data).not.toHaveProperty("password");
userId = response.body.data.id;
});
it("should return 400 for invalid email", async () => {
const response = await request(app)
.post("/api/v1/users")
.send({
email: "invalid-email",
username: "testuser",
password: "password123",
})
.expect(400);
expect(response.body).toHaveProperty("error");
expect(response.body.code).toBe("VALIDATION_ERROR");
});
it("should return 409 for duplicate email", async () => {
const response = await request(app)
.post("/api/v1/users")
.send({
email: "test@example.com",
username: "anotheruser",
password: "password123",
})
.expect(409);
expect(response.body.code).toBe("USER_EXISTS");
});
});
describe("GET /api/v1/users/:id", () => {
it("should return user by ID", async () => {
const response = await request(app)
.get(`/api/v1/users/${userId}`)
.expect(200);
expect(response.body.data.id).toBe(userId);
});
it("should return 404 for non-existent user", async () => {
const fakeId = "550e8400-e29b-41d4-a716-446655440000";
await request(app).get(`/api/v1/users/${fakeId}`).expect(404);
});
});
});
2. Integration Testing
# pytest API Integration Testing
import pytest
from httpx import AsyncClient
from app.main import app
from app.database import get_db
@pytest.fixture
async def client():
async with AsyncClient(app=app, base_url="http://test") as ac:
yield ac
@pytest.fixture
async def auth_token(client):
response = await client.post(
"/api/v1/auth/login",
json={
"email": "test@example.com",
"password": "testpassword"
}
)
return response.json()["token"]
@pytest.mark.asyncio
async def test_create_product_authenticated(client, auth_token):
response = await client.post(
"/api/v1/products",
headers={"Authorization": f"Bearer {auth_token}"},
json={
"name": "Test Product",
"price": 29.99,
"category": "Electronics"
}
)
assert response.status_code == 201
data = response.json()["data"]
assert data["name"] == "Test Product"
assert data["price"] == 29.99
@pytest.mark.asyncio
async def test_create_product_unauthorized(client):
response = await client.post(
"/api/v1/products",
json={
"name": "Test Product",
"price": 29.99
}
)
assert response.status_code == 401
3. API Monitoring
// API Health Check and Monitoring Endpoints
const express = require("express");
const router = express.Router();
const mongoose = require("mongoose");
const redis = require("redis");
router.get("/health", async (req, res) => {
const health = {
uptime: process.uptime(),
timestamp: Date.now(),
status: "OK",
checks: {},
};
try {
// Database check
const dbState = mongoose.connection.readyState;
health.checks.database = {
status: dbState === 1 ? "healthy" : "unhealthy",
responseTime: 0,
};
// Redis check
const redisStart = Date.now();
await redisClient.ping();
health.checks.redis = {
status: "healthy",
responseTime: Date.now() - redisStart,
};
// Check if all services are healthy
const allHealthy = Object.values(health.checks).every(
(check) => check.status === "healthy"
);
if (!allHealthy) {
health.status = "DEGRADED";
return res.status(503).json(health);
}
res.json(health);
} catch (error) {
health.status = "ERROR";
health.error = error.message;
res.status(503).json(health);
}
});
router.get("/metrics", (req, res) => {
// Prometheus-compatible metrics
res.set("Content-Type", "text/plain");
res.send(`
# HELP api_requests_total Total number of API requests
# TYPE api_requests_total counter
api_requests_total{method="GET",endpoint="/api/v1/users"} 1523
api_requests_total{method="POST",endpoint="/api/v1/users"} 342
# HELP api_request_duration_seconds API request duration
# TYPE api_request_duration_seconds histogram
api_request_duration_seconds_bucket{le="0.1"} 856
api_request_duration_seconds_bucket{le="0.5"} 1432
api_request_duration_seconds_bucket{le="1.0"} 1789
`);
});
module.exports = router;
Conclusion
Effective API design and architecture is crucial for building scalable, maintainable software systems. By following the principles and best practices outlined in this guide, you can create APIs that:
- Provide excellent developer experience through intuitive, consistent interfaces
- Scale efficiently with proper pagination, caching, and rate limiting
- Remain secure with robust authentication and authorization
- Evolve gracefully through versioning and backward compatibility
- Stay maintainable with comprehensive documentation and testing
Key Takeaways
- Design First: Define your API contract before implementation
- Be Consistent: Maintain uniform naming, structure, and error handling
- Think RESTful: Use HTTP methods and status codes correctly
- Secure by Default: Implement authentication, authorization, and rate limiting
- Document Everything: Provide clear, up-to-date API documentation
- Test Thoroughly: Write comprehensive unit and integration tests
- Monitor Continuously: Track performance and errors in production
Next Steps
- Choose your API architecture pattern (REST, GraphQL, gRPC)
- Define your API specification using OpenAPI or GraphQL schema
- Implement security and authentication mechanisms
- Create comprehensive documentation
- Set up testing and monitoring infrastructure
- Iterate based on developer feedback
Remember: Great APIs are built iteratively. Start with core functionality, gather feedback from developers, and continuously improve based on real-world usage.
Tools and Resources:
- Postman - API development and testing
- Swagger/OpenAPI - API documentation
- Insomnia - API client
- GraphiQL - GraphQL IDE
- Postman Learning Center - API tutorials
SEO Keywords: API design, RESTful API, API architecture, API best practices, GraphQL, API security, JWT authentication, API versioning, OpenAPI specification, API documentation, microservices, API testing, developer experience, HTTP methods, status codes, API patterns, backend development, web services
🤝 Need a Custom RSVP System or Dashboard?
I help businesses build tools that actually work , even on tight deadlines.
Whether you're planning an event, need internal tools, or want a custom dashboard for your team , I can help.
Reach out
📧 Email: safi.abdulkader@gmail.com | 💻 LinkedIn: @abdulkader-safi | 📱 Instagram: @abdulkader.safi | 🏢 DSRPT
Drop me a line, I’m always happy to collaborate! 🚀