O que são rotas no Slim e como o roteamento funciona
No Slim Framework, uma rota é o mapeamento entre um método HTTP (GET, POST, PUT, PATCH, DELETE etc.) e um caminho (path) que, quando acessado, executa um handler (função/ação). Esse handler recebe o Request e devolve um Response, e o Slim escolhe qual handler executar com base na combinação método + path.
Uma boa organização de rotas facilita: manutenção, versionamento de API, padronização REST, documentação mínima e separação por domínio (ex.: auth, users).
Definindo rotas por método HTTP (GET/POST/PUT/PATCH/DELETE)
GET: leitura
$app->get('/health', function ($request, $response) { $response->getBody()->write(json_encode(['status' => 'ok'])); return $response->withHeader('Content-Type', 'application/json') ->withStatus(200);});POST: criação
$app->post('/users', function ($request, $response) { $data = (array) $request->getParsedBody(); // validar e criar usuário... $created = ['id' => 123, 'name' => $data['name'] ?? null]; $response->getBody()->write(json_encode($created)); return $response->withHeader('Content-Type', 'application/json') ->withStatus(201);});PUT: substituição total do recurso
$app->put('/users/{id}', function ($request, $response, $args) { $id = (int) $args['id']; $data = (array) $request->getParsedBody(); // substituir todos os campos do usuário... $updated = ['id' => $id, 'name' => $data['name'] ?? null]; $response->getBody()->write(json_encode($updated)); return $response->withHeader('Content-Type', 'application/json') ->withStatus(200);});PATCH: atualização parcial
$app->patch('/users/{id}', function ($request, $response, $args) { $id = (int) $args['id']; $data = (array) $request->getParsedBody(); // atualizar apenas campos enviados... $updated = ['id' => $id] + $data; $response->getBody()->write(json_encode($updated)); return $response->withHeader('Content-Type', 'application/json') ->withStatus(200);});DELETE: remoção
$app->delete('/users/{id}', function ($request, $response, $args) { $id = (int) $args['id']; // remover usuário... return $response->withStatus(204); // sem body em 204});Rotas para múltiplos métodos
Quando o mesmo path aceita mais de um método, use map:
$app->map(['GET', 'HEAD'], '/docs', function ($request, $response) { $response->getBody()->write('API docs'); return $response->withStatus(200);});Parâmetros de rota (path params) e query params
Parâmetros no caminho
Parâmetros no path são definidos com chaves, por exemplo /users/{id}. O Slim disponibiliza esses valores em $args:
$app->get('/users/{id}', function ($request, $response, $args) { $id = (int) $args['id']; $user = ['id' => $id, 'name' => 'Ada']; $response->getBody()->write(json_encode($user)); return $response->withHeader('Content-Type', 'application/json') ->withStatus(200);});Query params
Filtros e paginação costumam ir na query string (?page=1&limit=20):
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
$app->get('/users', function ($request, $response) { $query = $request->getQueryParams(); $page = (int) ($query['page'] ?? 1); $limit = (int) ($query['limit'] ?? 20); $payload = [ 'page' => $page, 'limit' => $limit, 'items' => [], ]; $response->getBody()->write(json_encode($payload)); return $response->withHeader('Content-Type', 'application/json') ->withStatus(200);});Constraints (restrições) em parâmetros de rota
Para evitar que uma rota capture valores inválidos, aplique expressões regulares no parâmetro. Isso reduz ambiguidade e melhora mensagens de erro (ex.: não confundir /users/me com /users/{id}).
// Aceita apenas números para id$app->get('/users/{id:[0-9]+}', function ($request, $response, $args) { $id = (int) $args['id']; $response->getBody()->write(json_encode(['id' => $id])); return $response->withHeader('Content-Type', 'application/json') ->withStatus(200);});Exemplo com slug:
$app->get('/posts/{slug:[a-z0-9-]+}', function ($request, $response, $args) { $slug = $args['slug']; $response->getBody()->write(json_encode(['slug' => $slug])); return $response->withHeader('Content-Type', 'application/json') ->withStatus(200);});Rotas nomeadas e geração de URLs
Rotas nomeadas ajudam a referenciar endpoints internamente (ex.: em links, redirecionamentos, testes e documentação). No Slim, você pode nomear com ->setName().
$app->get('/users/{id:[0-9]+}', function ($request, $response, $args) { // ... return $response;})->setName('users.show');Para gerar a URL a partir do nome, use o RouteParser:
$app->get('/example-url', function ($request, $response) { $url = $this->get('routeParser')->urlFor('users.show', ['id' => 10]); $response->getBody()->write($url); return $response->withStatus(200);});Observação: a forma de obter o routeParser pode variar conforme seu container/bootstrapping. A ideia é: nomeie rotas críticas e use o parser para evitar hardcode de paths.
Agrupamento por prefixo (route groups) e organização por domínio
Quando a API cresce, agrupar rotas por prefixo e por contexto (domínio) reduz acoplamento e melhora a navegação do código.
Agrupando por prefixo
$app->group('/users', function ($group) { $group->get('', function ($request, $response) { // GET /users return $response; })->setName('users.index'); $group->post('', function ($request, $response) { // POST /users return $response; })->setName('users.store'); $group->get('/{id:[0-9]+}', function ($request, $response, $args) { // GET /users/{id} return $response; })->setName('users.show'); $group->patch('/{id:[0-9]+}', function ($request, $response, $args) { // PATCH /users/{id} return $response; })->setName('users.update'); $group->delete('/{id:[0-9]+}', function ($request, $response, $args) { // DELETE /users/{id} return $response; })->setName('users.destroy');});Organizando rotas em arquivos separados por contexto
Um padrão comum é ter um arquivo agregador que inclui arquivos de rotas por domínio. Exemplo de estrutura:
config/ routes.php routes/ auth.php users.php posts.phpPasso a passo para separar:
- Crie uma pasta
config/routes(ouroutes) - Crie um arquivo por domínio:
users.php,auth.php - No arquivo principal
routes.php, inclua esses arquivos e passe$app
Exemplo de config/routes.php:
<?phpdeclare(strict_types=1);use Slim\App;return function (App $app): void { (require __DIR__ . '/routes/auth.php')($app); (require __DIR__ . '/routes/users.php')($app);};Exemplo de config/routes/users.php:
<?phpdeclare(strict_types=1);use Slim\App;return function (App $app): void { $app->group('/users', function ($group) { $group->get('', UsersIndexAction::class)->setName('users.index'); $group->post('', UsersStoreAction::class)->setName('users.store'); $group->get('/{id:[0-9]+}', UsersShowAction::class)->setName('users.show'); $group->patch('/{id:[0-9]+}', UsersUpdateAction::class)->setName('users.update'); $group->delete('/{id:[0-9]+}', UsersDestroyAction::class)->setName('users.destroy'); });};Exemplo de config/routes/auth.php:
<?phpdeclare(strict_types=1);use Slim\App;return function (App $app): void { $app->group('/auth', function ($group) { $group->post('/login', AuthLoginAction::class)->setName('auth.login'); $group->post('/refresh', AuthRefreshAction::class)->setName('auth.refresh'); $group->post('/logout', AuthLogoutAction::class)->setName('auth.logout'); });};Note o uso de Action::class em vez de closures. Isso favorece organização e testabilidade, além de combinar bem com uma arquitetura em camadas (ex.: controllers/actions chamando serviços/casos de uso).
Versionamento de API com prefixo (/v1, /v2)
Uma forma simples e explícita de versionar é colocar a versão no path. Isso permite evoluir contratos sem quebrar clientes antigos.
$app->group('/v1', function ($v1) { $v1->group('/users', function ($group) { $group->get('', UsersIndexV1Action::class)->setName('v1.users.index'); $group->get('/{id:[0-9]+}', UsersShowV1Action::class)->setName('v1.users.show'); }); $v1->group('/auth', function ($group) { $group->post('/login', AuthLoginV1Action::class)->setName('v1.auth.login'); });});Boas práticas ao versionar:
- Evite mudar semântica de campos em uma versão existente; crie
/v2quando o contrato quebrar compatibilidade. - Mantenha rotas nomeadas com prefixo da versão para evitar colisões.
- Organize arquivos por versão quando necessário:
routes/v1/users.php,routes/v2/users.php.
Documentação mínima das rotas no próprio código
Mesmo sem uma ferramenta externa, você pode documentar o essencial diretamente onde a rota é definida: propósito, autenticação, payload, respostas e códigos de status. Isso reduz dúvidas e acelera manutenção.
Modelo de comentário por rota
/** * GET /v1/users * Lista usuários com paginação. * Auth: Bearer token (obrigatório) * Query: page (int, default 1), limit (int, default 20) * 200: { page, limit, items: [{id, name, ...}] } */$group->get('', UsersIndexV1Action::class)->setName('v1.users.index');Documentando criação (POST) com exemplos de payload
/** * POST /v1/users * Cria um usuário. * Auth: Bearer token (obrigatório) * Body (JSON): { "name": "Ada", "email": "ada@exemplo.com" } * 201: { "id": 123, "name": "Ada", "email": "ada@exemplo.com" } * 400: { "error": "validation_error", "fields": { ... } } */$group->post('', UsersStoreV1Action::class)->setName('v1.users.store');Padrões REST e convenções para endpoints
Convenções de nomenclatura
- Use substantivos no plural para coleções:
/users,/orders. - Use identificador no path para recurso:
/users/{id}. - Evite verbos no path quando o método HTTP já expressa a ação: prefira
POST /usersaPOST /createUser. - Para ações que não se encaixam bem em CRUD, use sub-recursos ou comandos explícitos:
POST /auth/login,POST /users/{id}/reset-password(com parcimônia).
Mapa REST básico (CRUD)
| Operação | Método | Endpoint | Status comum |
|---|---|---|---|
| Listar | GET | /users | 200 |
| Detalhar | GET | /users/{id} | 200, 404 |
| Criar | POST | /users | 201, 400 |
| Substituir | PUT | /users/{id} | 200 ou 204, 400, 404 |
| Atualizar parcial | PATCH | /users/{id} | 200 ou 204, 400, 404 |
| Remover | DELETE | /users/{id} | 204, 404 |
Códigos de status e payloads (prática recomendada)
- 200 OK: resposta com body (ex.: GET, PATCH retornando recurso).
- 201 Created: criação bem-sucedida; retorne o recurso criado e, se possível, header
Locationapontando para/users/{id}. - 204 No Content: operação bem-sucedida sem body (ex.: DELETE, PUT/PATCH quando não retorna conteúdo).
- 400 Bad Request: payload inválido, JSON malformado, validação falhou.
- 401 Unauthorized: token ausente/inválido.
- 403 Forbidden: autenticado, mas sem permissão.
- 404 Not Found: recurso não existe.
- 409 Conflict: conflito de estado (ex.: email já cadastrado).
- 422 Unprocessable Entity: validação semântica (muito usado em APIs; escolha 400 ou 422 e padronize).
Formato consistente de erro
Padronize erros para facilitar consumo por front-end e integrações:
{ "error": "validation_error", "message": "Invalid fields", "fields": { "email": "must be a valid email" }}Passo a passo: montando um conjunto de rotas bem organizado
1) Defina o prefixo de versão
$app->group('/v1', function ($v1) { // grupos por domínio aqui});2) Crie grupos por domínio (users, auth)
$v1->group('/users', function ($users) { $users->get('', UsersIndexV1Action::class)->setName('v1.users.index'); $users->post('', UsersStoreV1Action::class)->setName('v1.users.store'); $users->get('/{id:[0-9]+}', UsersShowV1Action::class)->setName('v1.users.show'); $users->patch('/{id:[0-9]+}', UsersUpdateV1Action::class)->setName('v1.users.update'); $users->delete('/{id:[0-9]+}', UsersDestroyV1Action::class)->setName('v1.users.destroy');});$v1->group('/auth', function ($auth) { $auth->post('/login', AuthLoginV1Action::class)->setName('v1.auth.login'); $auth->post('/refresh', AuthRefreshV1Action::class)->setName('v1.auth.refresh'); $auth->post('/logout', AuthLogoutV1Action::class)->setName('v1.auth.logout');});3) Aplique constraints para evitar rotas ambíguas
$users->get('/{id:[0-9]+}', UsersShowV1Action::class);$users->get('/me', UsersMeV1Action::class);Com a constraint, /users/me não será interpretado como id.
4) Nomeie rotas importantes
Nomeie principalmente rotas que serão referenciadas por outros pontos do sistema (links, testes, redirecionamentos, documentação):
$auth->post('/login', AuthLoginV1Action::class)->setName('v1.auth.login');5) Documente o contrato mínimo ao lado da rota
/** * PATCH /v1/users/{id} * Atualiza parcialmente um usuário. * Auth: Bearer token * Body (JSON): { "name"?: string, "email"?: string } * 200: { "id": number, "name": string, "email": string } * 404: { "error": "not_found" } */$users->patch('/{id:[0-9]+}', UsersUpdateV1Action::class)->setName('v1.users.update');