Princípios de Design de REST API - Design de API Escalável e Manutenível

Intermediário | 2025.12.02

Princípios Básicos da REST API

REST (Representational State Transfer) é um estilo arquitetural para design de serviços web. É projetado com base em 6 restrições.

flowchart TB
    subgraph REST["REST - 6 Restrições"]
        CS["1. Client-Server<br/>Separação Cliente-Servidor"]
        SL["2. Stateless<br/>Cada requisição é independente"]
        CA["3. Cacheable<br/>Indicar se é cacheável"]
        UI["4. Uniform Interface<br/>Interface Uniforme"]
        LS["5. Layered System<br/>Sistema em Camadas"]
        CD["6. Code on Demand<br/>(Opcional)"]
    end

    Client["Client<br/>(UI)"] <-->|HTTP| Server["Server<br/>(Data)"]

Design de Recursos

Convenções de Nomenclatura

Bons exemplos:

  • /users - Plural, substantivo
  • /users/123 - ID do recurso
  • /users/123/orders - Recurso aninhado
  • /users/123/orders/456 - Sub-recurso específico

Maus exemplos:

  • /getUsers - Não use verbos
  • /user - Evite singular
  • /Users - Evite maiúsculas
  • /user-list - Representação de lista desnecessária
  • /api/v1/get-all-users - Verbo + expressão redundante

Relação hierárquica:

/organizations/{orgId}
/organizations/{orgId}/teams/{teamId}
/organizations/{orgId}/teams/{teamId}/members

Evite aninhamentos profundos (até 3 níveis):

  • /organizations/{id}/teams/{id}/projects/{id}/tasks
  • /tasks?projectId={id}

Coleções e Documentos

Estrutura de recursos:

/users                  → Coleção (User[])
/users/123              → Documento (User)
/users/123/avatar       → Sub-recurso (único)
/users/123/orders       → Sub-coleção (Order[])
/users/123/orders/456   → Sub-documento (Order)

Uso de Métodos HTTP

MétodoUsoIdempotênciaSegurança
GETObtenção de recurso
POSTCriação de recurso××
PUTSubstituição completa do recurso×
PATCHAtualização parcial do recurso××
DELETEExclusão do recurso×
HEADObtenção de metadados
OPTIONSVerificação de métodos disponíveis
// Exemplo de implementação com Express.js
import express from 'express';

const router = express.Router();

// GET: Obtenção de recurso
router.get('/users', async (req, res) => {
  const users = await userService.findAll(req.query);
  res.json({ data: users });
});

router.get('/users/:id', async (req, res) => {
  const user = await userService.findById(req.params.id);
  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }
  res.json({ data: user });
});

// POST: Criação de recurso
router.post('/users', async (req, res) => {
  const user = await userService.create(req.body);
  res.status(201).json({ data: user });
});

// PUT: Substituição completa do recurso
router.put('/users/:id', async (req, res) => {
  const user = await userService.replace(req.params.id, req.body);
  res.json({ data: user });
});

// PATCH: Atualização parcial do recurso
router.patch('/users/:id', async (req, res) => {
  const user = await userService.update(req.params.id, req.body);
  res.json({ data: user });
});

// DELETE: Exclusão do recurso
router.delete('/users/:id', async (req, res) => {
  await userService.delete(req.params.id);
  res.status(204).send();
});

Códigos de Status HTTP

Lista de Códigos de Status

2xx Sucesso:

CódigoDescrição
200 OKSucesso (GET, PUT, PATCH)
201 CreatedCriação bem-sucedida (POST)
204 No ContentSucesso, sem corpo de resposta (DELETE)

3xx Redirecionamento:

CódigoDescrição
301 Moved PermanentlyMovido permanentemente
304 Not ModifiedCache válido

4xx Erro do Cliente:

CódigoDescrição
400 Bad RequestRequisição inválida
401 UnauthorizedAutenticação necessária
403 ForbiddenSem permissão de acesso
404 Not FoundRecurso não existe
405 Method Not AllowedMétodo não permitido
409 ConflictConflito de recurso
422 Unprocessable EntityErro de validação
429 Too Many RequestsLimite de taxa excedido

5xx Erro do Servidor:

CódigoDescrição
500 Internal Server ErrorErro interno
502 Bad GatewayErro de gateway
503 Service UnavailableServiço indisponível
504 Gateway TimeoutTimeout

Design de Resposta de Erro

// Formato de resposta de erro unificado
interface ErrorResponse {
  error: {
    code: string;           // Código de erro legível por máquina
    message: string;        // Mensagem legível por humanos
    details?: ErrorDetail[]; // Informações detalhadas (erros de validação, etc.)
    requestId?: string;     // ID da requisição para debug
    documentation?: string; // Link para documentação
  };
}

interface ErrorDetail {
  field: string;
  message: string;
  code: string;
}

// Exemplo de implementação
class ApiError extends Error {
  constructor(
    public statusCode: number,
    public code: string,
    message: string,
    public details?: ErrorDetail[]
  ) {
    super(message);
  }
}

// Middleware de tratamento de erros
function errorHandler(err: Error, req: Request, res: Response, next: NextFunction) {
  const requestId = req.headers['x-request-id'] || generateRequestId();

  if (err instanceof ApiError) {
    return res.status(err.statusCode).json({
      error: {
        code: err.code,
        message: err.message,
        details: err.details,
        requestId,
      },
    });
  }

  // Erro inesperado
  console.error(`[${requestId}] Unexpected error:`, err);
  return res.status(500).json({
    error: {
      code: 'INTERNAL_ERROR',
      message: 'An unexpected error occurred',
      requestId,
    },
  });
}
// Exemplo de erro de validação
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed",
    "details": [
      {
        "field": "email",
        "message": "Invalid email format",
        "code": "INVALID_FORMAT"
      },
      {
        "field": "password",
        "message": "Password must be at least 8 characters",
        "code": "TOO_SHORT"
      }
    ],
    "requestId": "req_abc123"
  }
}

Paginação

MétodoExemploVantagensDesvantagens
Baseado em OffsetGET /users?limit=10&offset=20Simples, possível pular para qualquer páginaLento com grandes volumes, duplicação/omissão ao adicionar dados
Baseado em CursorGET /users?limit=10&cursor=eyJpZCI6MTAwfQRápido, resistente a mudanças de dadosNão é possível pular para página específica
Baseado em KeysetGET /users?limit=10&after_id=100Simples e rápidoDepende da ordem de classificação

Recomendado: Dados em tempo real → Cursor / Dados estáticos → Offset

Exemplo de Implementação

// Paginação baseada em cursor
interface PaginatedResponse<T> {
  data: T[];
  pagination: {
    cursor: string | null;
    hasMore: boolean;
    totalCount?: number;
  };
}

async function getUsers(cursor?: string, limit = 10): Promise<PaginatedResponse<User>> {
  let query = db.users.orderBy('createdAt', 'desc');

  if (cursor) {
    const decoded = decodeCursor(cursor);
    query = query.where('createdAt', '<', decoded.createdAt);
  }

  const users = await query.limit(limit + 1).exec();
  const hasMore = users.length > limit;
  const data = hasMore ? users.slice(0, -1) : users;

  return {
    data,
    pagination: {
      cursor: data.length > 0 ? encodeCursor(data[data.length - 1]) : null,
      hasMore,
    },
  };
}

function encodeCursor(user: User): string {
  return Buffer.from(JSON.stringify({ id: user.id, createdAt: user.createdAt })).toString('base64');
}

function decodeCursor(cursor: string): { id: string; createdAt: Date } {
  return JSON.parse(Buffer.from(cursor, 'base64').toString());
}

Versionamento

MétodoExemploVantagensDesvantagens
Caminho URL/api/v1/usersClaro, fácil de cachearRequer mudança de URL
Parâmetro de Query/api/users?version=1FlexívelDifícil de cachear
HeaderAccept: application/vnd.api+json;version=1URL limpaDifícil de descobrir
Media TypeAccept: application/vnd.myapi.v1+jsonPadrãoComplexo
// Método de caminho URL (recomendado)
app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);

// Método de header
app.use((req, res, next) => {
  const version = req.headers['api-version'] || '1';
  req.apiVersion = parseInt(version);
  next();
});

Filtragem, Ordenação e Busca

# Filtragem
GET /users?status=active&role=admin
GET /orders?created_after=2024-01-01&created_before=2024-12-31
GET /products?price_min=100&price_max=500

# Ordenação
GET /users?sort=created_at          # Ascendente
GET /users?sort=-created_at         # Descendente
GET /users?sort=name,-created_at    # Múltiplos campos

# Busca
GET /users?q=john                   # Busca de texto completo
GET /users?search[name]=john        # Especificação de campo

# Seleção de campos
GET /users?fields=id,name,email     # Apenas campos necessários
GET /users?include=orders,profile   # Inclusão de relacionamentos
// Implementação do query builder
function buildQuery(params: QueryParams) {
  let query = db.users;

  // Filtragem
  if (params.status) {
    query = query.where('status', '=', params.status);
  }

  // Ordenação
  if (params.sort) {
    const fields = params.sort.split(',');
    for (const field of fields) {
      const order = field.startsWith('-') ? 'desc' : 'asc';
      const column = field.replace(/^-/, '');
      query = query.orderBy(column, order);
    }
  }

  // Seleção de campos
  if (params.fields) {
    const columns = params.fields.split(',');
    query = query.select(columns);
  }

  return query;
}

Limitação de Taxa

// Headers de limitação de taxa
app.use((req, res, next) => {
  const rateLimit = getRateLimit(req);

  res.set({
    'X-RateLimit-Limit': rateLimit.limit,
    'X-RateLimit-Remaining': rateLimit.remaining,
    'X-RateLimit-Reset': rateLimit.resetAt,
  });

  if (rateLimit.remaining <= 0) {
    return res.status(429).json({
      error: {
        code: 'RATE_LIMIT_EXCEEDED',
        message: 'Too many requests',
        retryAfter: rateLimit.resetAt,
      },
    });
  }

  next();
});

Autenticação e Autorização

// Autenticação Bearer Token
const authMiddleware = async (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.substring(7);

  try {
    const payload = await verifyToken(token);
    req.user = payload;
    next();
  } catch (err) {
    return res.status(401).json({
      error: {
        code: 'INVALID_TOKEN',
        message: 'Token is invalid or expired',
      },
    });
  }
};

// Autorização baseada em papéis
const requireRole = (...roles: string[]) => {
  return (req: Request, res: Response, next: NextFunction) => {
    if (!roles.includes(req.user.role)) {
      return res.status(403).json({
        error: {
          code: 'FORBIDDEN',
          message: 'Insufficient permissions',
        },
      });
    }
    next();
  };
};

// Exemplo de uso
router.delete('/users/:id', authMiddleware, requireRole('admin'), deleteUser);

Documentação OpenAPI (Swagger)

# openapi.yaml
openapi: 3.0.3
info:
  title: User API
  version: 1.0.0
  description: User management API

paths:
  /users:
    get:
      summary: List all users
      tags: [Users]
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            default: 10
        - name: cursor
          in: query
          schema:
            type: string
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserListResponse'

components:
  schemas:
    User:
      type: object
      properties:
        id:
          type: string
        email:
          type: string
          format: email
        name:
          type: string
      required: [id, email, name]
← Voltar para a lista