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étodo | Uso | Idempotência | Segurança |
|---|---|---|---|
| GET | Obtenção de recurso | ○ | ○ |
| POST | Criação de recurso | × | × |
| PUT | Substituição completa do recurso | ○ | × |
| PATCH | Atualização parcial do recurso | × | × |
| DELETE | Exclusão do recurso | ○ | × |
| HEAD | Obtenção de metadados | ○ | ○ |
| OPTIONS | Verificaçã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ódigo | Descrição |
|---|---|
| 200 OK | Sucesso (GET, PUT, PATCH) |
| 201 Created | Criação bem-sucedida (POST) |
| 204 No Content | Sucesso, sem corpo de resposta (DELETE) |
3xx Redirecionamento:
| Código | Descrição |
|---|---|
| 301 Moved Permanently | Movido permanentemente |
| 304 Not Modified | Cache válido |
4xx Erro do Cliente:
| Código | Descrição |
|---|---|
| 400 Bad Request | Requisição inválida |
| 401 Unauthorized | Autenticação necessária |
| 403 Forbidden | Sem permissão de acesso |
| 404 Not Found | Recurso não existe |
| 405 Method Not Allowed | Método não permitido |
| 409 Conflict | Conflito de recurso |
| 422 Unprocessable Entity | Erro de validação |
| 429 Too Many Requests | Limite de taxa excedido |
5xx Erro do Servidor:
| Código | Descrição |
|---|---|
| 500 Internal Server Error | Erro interno |
| 502 Bad Gateway | Erro de gateway |
| 503 Service Unavailable | Serviço indisponível |
| 504 Gateway Timeout | Timeout |
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étodo | Exemplo | Vantagens | Desvantagens |
|---|---|---|---|
| Baseado em Offset | GET /users?limit=10&offset=20 | Simples, possível pular para qualquer página | Lento com grandes volumes, duplicação/omissão ao adicionar dados |
| Baseado em Cursor | GET /users?limit=10&cursor=eyJpZCI6MTAwfQ | Rápido, resistente a mudanças de dados | Não é possível pular para página específica |
| Baseado em Keyset | GET /users?limit=10&after_id=100 | Simples e rápido | Depende 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étodo | Exemplo | Vantagens | Desvantagens |
|---|---|---|---|
| Caminho URL | /api/v1/users | Claro, fácil de cachear | Requer mudança de URL |
| Parâmetro de Query | /api/users?version=1 | Flexível | Difícil de cachear |
| Header | Accept: application/vnd.api+json;version=1 | URL limpa | Difícil de descobrir |
| Media Type | Accept: application/vnd.myapi.v1+json | Padrão | Complexo |
// 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]
Links de Referência
- RESTful Web APIs (O’Reilly)
- HTTP API Design Guide
- Microsoft REST API Guidelines
- JSON:API Specification