10 KiB
10 KiB
Backend API Design
Comprehensive guide to designing RESTful, GraphQL, and gRPC APIs with best practices (2025).
REST API Design
Resource-Based URLs
Good:
GET /api/v1/users # List users
GET /api/v1/users/:id # Get specific user
POST /api/v1/users # Create user
PUT /api/v1/users/:id # Update user (full)
PATCH /api/v1/users/:id # Update user (partial)
DELETE /api/v1/users/:id # Delete user
GET /api/v1/users/:id/posts # Get user's posts
POST /api/v1/users/:id/posts # Create post for user
Bad (Avoid):
GET /api/v1/getUser?id=123 # RPC-style, not RESTful
POST /api/v1/createUser # Verb in URL
GET /api/v1/user-posts # Unclear relationship
HTTP Status Codes (Meaningful Responses)
Success:
200 OK- Successful GET, PUT, PATCH201 Created- Successful POST (resource created)204 No Content- Successful DELETE
Client Errors:
400 Bad Request- Invalid input/validation error401 Unauthorized- Missing or invalid authentication403 Forbidden- Authenticated but not authorized404 Not Found- Resource doesn't exist409 Conflict- Resource conflict (duplicate email)422 Unprocessable Entity- Validation error (detailed)429 Too Many Requests- Rate limit exceeded
Server Errors:
500 Internal Server Error- Generic server error502 Bad Gateway- Upstream service error503 Service Unavailable- Temporary downtime504 Gateway Timeout- Upstream service timeout
Request/Response Format
Request:
POST /api/v1/users
Content-Type: application/json
{
"email": "user@example.com",
"name": "John Doe",
"age": 30
}
Success Response:
HTTP/1.1 201 Created
Content-Type: application/json
Location: /api/v1/users/123
{
"id": "123",
"email": "user@example.com",
"name": "John Doe",
"age": 30,
"createdAt": "2025-01-09T12:00:00Z",
"updatedAt": "2025-01-09T12:00:00Z"
}
Error Response:
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid input data",
"details": [
{
"field": "email",
"message": "Invalid email format",
"value": "invalid-email"
},
{
"field": "age",
"message": "Age must be between 18 and 120",
"value": 15
}
],
"timestamp": "2025-01-09T12:00:00Z",
"path": "/api/v1/users"
}
}
Pagination
// Request
GET /api/v1/users?page=2&limit=50
// Response
{
"data": [...],
"pagination": {
"page": 2,
"limit": 50,
"total": 1234,
"totalPages": 25,
"hasNext": true,
"hasPrev": true
},
"links": {
"first": "/api/v1/users?page=1&limit=50",
"prev": "/api/v1/users?page=1&limit=50",
"next": "/api/v1/users?page=3&limit=50",
"last": "/api/v1/users?page=25&limit=50"
}
}
Filtering and Sorting
GET /api/v1/users?status=active&role=admin&sort=-createdAt,name&limit=20
# Filters: status=active AND role=admin
# Sort: createdAt DESC, name ASC
# Limit: 20 results
API Versioning Strategies
URL Versioning (Most Common):
/api/v1/users
/api/v2/users
Header Versioning:
GET /api/users
Accept: application/vnd.myapi.v2+json
Query Parameter:
/api/users?version=2
Recommendation: URL versioning for simplicity and discoverability
GraphQL API Design
Schema Definition
type User {
id: ID!
email: String!
name: String!
posts: [Post!]!
createdAt: DateTime!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
published: Boolean!
createdAt: DateTime!
}
type Query {
user(id: ID!): User
users(limit: Int = 50, offset: Int = 0): [User!]!
post(id: ID!): Post
posts(authorId: ID, published: Boolean): [Post!]!
}
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
deleteUser(id: ID!): Boolean!
createPost(input: CreatePostInput!): Post!
publishPost(id: ID!): Post!
}
input CreateUserInput {
email: String!
name: String!
password: String!
}
input UpdateUserInput {
email: String
name: String
}
Queries
# Flexible data fetching - client specifies exactly what they need
query {
user(id: "123") {
id
name
email
posts {
id
title
published
}
}
}
# With variables
query GetUser($userId: ID!) {
user(id: $userId) {
id
name
posts(published: true) {
title
}
}
}
Mutations
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
email
name
createdAt
}
}
# Variables
{
"input": {
"email": "user@example.com",
"name": "John Doe",
"password": "SecurePass123!"
}
}
Resolvers (NestJS Example)
@Resolver(() => User)
export class UserResolver {
constructor(
private userService: UserService,
private postService: PostService,
) {}
@Query(() => User, { nullable: true })
async user(@Args('id') id: string) {
return this.userService.findById(id);
}
@Query(() => [User])
async users(
@Args('limit', { defaultValue: 50 }) limit: number,
@Args('offset', { defaultValue: 0 }) offset: number,
) {
return this.userService.findAll({ limit, offset });
}
@Mutation(() => User)
async createUser(@Args('input') input: CreateUserInput) {
return this.userService.create(input);
}
// Field resolver - lazy load posts
@ResolveField(() => [Post])
async posts(@Parent() user: User) {
return this.postService.findByAuthorId(user.id);
}
}
GraphQL Best Practices
- Avoid N+1 Problem - Use DataLoader
import DataLoader from 'dataloader';
const postLoader = new DataLoader(async (authorIds: string[]) => {
const posts = await db.posts.findAll({ where: { authorId: authorIds } });
return authorIds.map(id => posts.filter(p => p.authorId === id));
});
// In resolver
@ResolveField(() => [Post])
async posts(@Parent() user: User) {
return this.postLoader.load(user.id);
}
- Pagination - Relay-style cursor pagination
- Error Handling - Return errors in response
- Depth Limiting - Prevent deeply nested queries
- Query Complexity Analysis - Limit expensive queries
gRPC API Design
Protocol Buffers Schema
syntax = "proto3";
package user;
service UserService {
rpc GetUser (GetUserRequest) returns (User);
rpc ListUsers (ListUsersRequest) returns (ListUsersResponse);
rpc CreateUser (CreateUserRequest) returns (User);
rpc UpdateUser (UpdateUserRequest) returns (User);
rpc DeleteUser (DeleteUserRequest) returns (DeleteUserResponse);
// Streaming
rpc StreamUsers (StreamUsersRequest) returns (stream User);
}
message User {
string id = 1;
string email = 2;
string name = 3;
int64 created_at = 4;
}
message GetUserRequest {
string id = 1;
}
message ListUsersRequest {
int32 limit = 1;
int32 offset = 2;
}
message ListUsersResponse {
repeated User users = 1;
int32 total = 2;
}
message CreateUserRequest {
string email = 1;
string name = 2;
string password = 3;
}
Implementation (Node.js)
import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader';
const packageDefinition = protoLoader.loadSync('user.proto');
const userProto = grpc.loadPackageDefinition(packageDefinition).user;
// Server implementation
const server = new grpc.Server();
server.addService(userProto.UserService.service, {
async getUser(call, callback) {
const user = await userService.findById(call.request.id);
callback(null, user);
},
async createUser(call, callback) {
const user = await userService.create(call.request);
callback(null, user);
},
async streamUsers(call) {
const users = await userService.findAll();
for (const user of users) {
call.write(user);
}
call.end();
},
});
server.bindAsync(
'0.0.0.0:50051',
grpc.ServerCredentials.createInsecure(),
() => server.start()
);
gRPC Benefits
- Performance: 7-10x faster than REST (binary protocol)
- Streaming: Bi-directional streaming
- Type Safety: Strong typing via Protocol Buffers
- Code Generation: Auto-generate client/server code
- Best For: Internal microservices, high-performance systems
API Design Decision Matrix
| Feature | REST | GraphQL | gRPC |
|---|---|---|---|
| Use Case | Public APIs, CRUD | Flexible data fetching | Microservices, performance |
| Performance | Moderate | Moderate | Fastest (7-10x REST) |
| Caching | HTTP caching built-in | Complex | No built-in caching |
| Browser Support | Native | Native | Requires gRPC-Web |
| Learning Curve | Easy | Moderate | Steep |
| Streaming | Limited (SSE) | Subscriptions | Bi-directional |
| Tooling | Excellent | Excellent | Good |
| Documentation | OpenAPI/Swagger | Schema introspection | Protobuf definition |
API Security Checklist
- HTTPS/TLS only (no HTTP)
- Authentication (OAuth 2.1, JWT, API keys)
- Authorization (RBAC, check permissions)
- Rate limiting (prevent abuse)
- Input validation (all endpoints)
- CORS configured properly
- Security headers (CSP, HSTS, X-Frame-Options)
- API versioning implemented
- Error messages don't leak system info
- Audit logging (who did what, when)
API Documentation
OpenAPI/Swagger (REST)
openapi: 3.0.0
info:
title: User API
version: 1.0.0
paths:
/api/v1/users:
get:
summary: List users
parameters:
- name: limit
in: query
schema:
type: integer
default: 50
responses:
'200':
description: Successful response
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/User'
components:
schemas:
User:
type: object
properties:
id:
type: string
email:
type: string
name:
type: string
Resources
- REST Best Practices: https://restfulapi.net/
- GraphQL: https://graphql.org/learn/
- gRPC: https://grpc.io/docs/
- OpenAPI: https://swagger.io/specification/