Rotas no Slim Framework: definição, agrupamento e organização por domínio

Capítulo 3

Tempo estimado de leitura: 10 minutos

+ Exercício

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):

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

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.php

Passo a passo para separar:

  • Crie uma pasta config/routes (ou routes)
  • 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 /v2 quando 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 /users a POST /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çãoMétodoEndpointStatus comum
ListarGET/users200
DetalharGET/users/{id}200, 404
CriarPOST/users201, 400
SubstituirPUT/users/{id}200 ou 204, 400, 404
Atualizar parcialPATCH/users/{id}200 ou 204, 400, 404
RemoverDELETE/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 Location apontando 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');

Agora responda o exercício sobre o conteúdo:

Ao definir rotas no Slim Framework, qual prática ajuda a evitar ambiguidade entre caminhos como /users/me e /users/{id}?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

Constraints com regex limitam quais valores o parâmetro pode capturar (ex.: apenas números em {id:[0-9]+}), evitando que um path fixo como /users/me seja interpretado como id.

Próximo capitúlo

Controllers/Actions no Slim Framework: handlers enxutos e focados

Arrow Right Icon
Capa do Ebook gratuito Back-end com Slim Framework (PHP): Rotas, Middlewares e Arquitetura Limpa
19%

Back-end com Slim Framework (PHP): Rotas, Middlewares e Arquitetura Limpa

Novo curso

16 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.