RESTful API Design: Best Practices and Modern Approaches
Backend13 min read

RESTful API Design: Best Practices and Modern Approaches

Comprehensive guide to professional RESTful API design. Endpoint naming, versioning, authentication, error handling, and documentation best practices.

Taha Kocal

Taha Kocal

Full Stack Developer

Nov 15, 2024
#API#REST#Backend#Node.js#Best Practices

RESTful API design is the practice of structuring web APIs around resources, standard HTTP methods, and predictable conventions so that clients can create, read, update, and delete data over HTTP in a consistent way. A well-designed REST API uses plural nouns in URLs (such as /api/v1/users), maps HTTP verbs to actions (GET for reads, POST for creation, PUT and PATCH for updates, DELETE for removal), and returns accurate status codes like 200, 201, 404, and 422. Beyond naming, professional API design covers versioning through the URL path, JWT-based authentication with role-based authorization, rate limiting, request validation, and a uniform JSON response envelope for both success and error payloads. These conventions matter because APIs are contracts: consistent, intuitive patterns improve developer experience, reduce integration bugs, and cut support overhead. This guide walks through each practice with TypeScript and Express examples.

REST Fundamentals

HTTP Methods

Standard HTTP methods and their purposes:

  • GET: Read resources (idempotent, safe)
  • POST: Create new resources
  • PUT: Full resource update (idempotent)
  • PATCH: Partial resource update
  • DELETE: Remove resources (idempotent)

HTTP Status Codes

2xx Success codes:

  • 200 OK - General success
  • 201 Created - Resource created successfully
  • 204 No Content - Success with no response body

4xx Client error codes:

  • 400 Bad Request - Invalid request format
  • 401 Unauthorized - Authentication required
  • 403 Forbidden - Insufficient permissions
  • 404 Not Found - Resource not found
  • 422 Unprocessable Entity - Validation error

How Should You Design Endpoints?

bash
# Good - Plural nouns, kebab-case
GET    /api/v1/users
GET    /api/v1/users/123
POST   /api/v1/users
PUT    /api/v1/users/123
DELETE /api/v1/users/123

GET    /api/v1/users/123/orders
GET    /api/v1/order-items

# Bad - Avoid verbs in URLs
GET    /api/v1/getUsers
POST   /api/v1/createUser
DELETE /api/v1/deleteUser/123

Filtering, Sorting, Pagination

bash
# Filtering
GET /api/v1/products?category=electronics&price_min=100&price_max=500

# Sorting
GET /api/v1/products?sort=price&order=desc
GET /api/v1/products?sort=-price,+name

# Pagination
GET /api/v1/products?page=2&limit=20

# Combined
GET /api/v1/products?category=electronics&sort=-price&page=1&limit=20

What Should Your Response Format Look Like?

Success Response

typescript
// Single resource
{
  "data": {
    "id": "123",
    "type": "user",
    "attributes": {
      "name": "John Doe",
      "email": "john@example.com",
      "createdAt": "2024-01-15T10:30:00Z"
    }
  }
}

// List with pagination
{
  "data": [
    { "id": "1", "name": "Product 1" },
    { "id": "2", "name": "Product 2" }
  ],
  "meta": {
    "total": 100,
    "page": 1,
    "limit": 20,
    "totalPages": 5
  },
  "links": {
    "self": "/api/v1/products?page=1",
    "next": "/api/v1/products?page=2",
    "first": "/api/v1/products?page=1",
    "last": "/api/v1/products?page=5"
  }
}

Error Response

typescript
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Validation failed",
    "details": [
      {
        "field": "email",
        "message": "Invalid email format",
        "code": "INVALID_FORMAT"
      },
      {
        "field": "password",
        "message": "Password must be at least 8 characters",
        "code": "MIN_LENGTH"
      }
    ],
    "timestamp": "2024-01-15T10:30:00Z",
    "requestId": "req_abc123"
  }
}

TypeScript Interfaces

typescript
interface ApiResponse<T> {
  data: T;
  meta?: PaginationMeta;
  links?: PaginationLinks;
}

interface ApiError {
  error: {
    code: string;
    message: string;
    details?: ErrorDetail[];
    timestamp: string;
    requestId: string;
  };
}

interface PaginationMeta {
  total: number;
  page: number;
  limit: number;
  totalPages: number;
}

// Usage
type GetUsersResponse = ApiResponse<User[]>;
type GetUserResponse = ApiResponse<User>;

How Should You Version an API?

URL versioning is the most common and recommended approach. It makes the API version explicit and easy to understand.

typescript
// Deprecation headers for old versions
app.use('/api/v1', (req, res, next) => {
  res.set('Deprecation', 'true');
  res.set('Sunset', 'Sat, 31 Dec 2024 23:59:59 GMT');
  res.set('Link', '</api/v2>; rel="successor-version"');
  next();
});

Authentication & Authorization

JWT Authentication

typescript
import jwt from 'jsonwebtoken';

async function authMiddleware(req: Request, res: Response, next: NextFunction) {
  const authHeader = req.headers.authorization;

  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({
      error: {
        code: 'UNAUTHORIZED',
        message: 'Missing or invalid authorization header',
      },
    });
  }

  const token = authHeader.split(' ')[1];

  try {
    const payload = jwt.verify(token, process.env.JWT_SECRET!);
    req.user = payload;
    next();
  } catch (error) {
    return res.status(401).json({
      error: {
        code: 'INVALID_TOKEN',
        message: 'Token is invalid or expired',
      },
    });
  }
}

Role-Based Access Control

typescript
function authorize(...allowedRoles: UserRole[]) {
  return (req: Request, res: Response, next: NextFunction) => {
    if (!req.user) {
      return res.status(401).json({
        error: { code: 'UNAUTHORIZED', message: 'Not authenticated' },
      });
    }

    if (!allowedRoles.includes(req.user.role)) {
      return res.status(403).json({
        error: { code: 'FORBIDDEN', message: 'Insufficient permissions' },
      });
    }

    next();
  };
}

// Usage
app.delete('/api/v1/users/:id', authMiddleware, authorize('admin'), deleteUser);

Rate Limiting

typescript
import rateLimit from 'express-rate-limit';

const generalLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // 100 requests per window
  message: {
    error: {
      code: 'RATE_LIMIT_EXCEEDED',
      message: 'Too many requests, please try again later',
    },
  },
  standardHeaders: true,
  legacyHeaders: false,
});

app.use('/api/', generalLimiter);

Validation with Zod

typescript
import { z } from 'zod';

const createUserSchema = z.object({
  name: z.string().min(2).max(100),
  email: z.string().email(),
  password: z.string().min(8).regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/),
  role: z.enum(['user', 'admin']).default('user'),
});

type CreateUserInput = z.infer<typeof createUserSchema>;

function validate<T>(schema: z.ZodSchema<T>) {
  return (req: Request, res: Response, next: NextFunction) => {
    const result = schema.safeParse(req.body);

    if (!result.success) {
      return res.status(422).json({
        error: {
          code: 'VALIDATION_ERROR',
          message: 'Validation failed',
          details: result.error.errors.map((err) => ({
            field: err.path.join('.'),
            message: err.message,
          })),
        },
      });
    }

    req.body = result.data;
    next();
  };
}

app.post('/api/v1/users', validate(createUserSchema), createUser);

A well-designed API should be consistent, intuitive, secure, performant, and evolvable. Apply these principles to create an excellent experience for developers using your API.

Remember that API design is about communication. Clear, consistent patterns make your API a joy to work with and reduce support overhead significantly.

Share this article

Related Articles