Projeto final guiado: API com Slim Framework aplicando rotas, middlewares e Arquitetura Limpa

Capítulo 16

Tempo estimado de leitura: 11 minutos

+ Exercício

Objetivo do projeto e escopo

Neste projeto final, você vai implementar uma API completa de exemplo para gerenciamento de usuários e autenticação, aplicando em conjunto: rotas organizadas por domínio, handlers enxutos, validação/sanitização, middlewares (CORS, auth, logging), DI, configuração por ambiente, tratamento padronizado de erros e persistência via repositórios. A proposta é entregar um esqueleto realista, pronto para crescer com novas funcionalidades sem comprometer a arquitetura.

Funcionalidades do MVP

  • Auth: login e refresh (ou re-login) retornando JWT.
  • Users: criar usuário, listar usuários (paginado), obter usuário por id, atualizar e desativar.
  • Infra: CORS, logging, autenticação via middleware, erros padronizados, repositórios com transação quando necessário.

Arquitetura e estrutura de pastas (referência do projeto)

Use uma estrutura que deixe explícitas as fronteiras entre HTTP (Slim), aplicação (casos de uso) e infraestrutura (DB, JWT, logger). Um exemplo:

project/  ├─ public/  │  └─ index.php  ├─ config/  │  ├─ settings.php  │  └─ routes.php  ├─ src/  │  ├─ Http/  │  │  ├─ Actions/  │  │  ├─ Middlewares/  │  │  └─ Response/  │  ├─ Application/  │  │  ├─ UseCase/  │  │  ├─ DTO/  │  │  └─ Service/  │  ├─ Domain/  │  │  ├─ Entity/  │  │  └─ Repository/  │  └─ Infrastructure/  │     ├─ Persistence/  │     ├─ Security/  │     └─ Logging/  ├─ tests/  └─ composer.json

Regra prática: o Slim (rotas/middlewares/actions) conhece a camada de aplicação, mas a camada de aplicação não conhece Slim. Repositórios são interfaces no domínio e implementações na infraestrutura.

Contrato de resposta e convenções da API

Defina um formato consistente para respostas de sucesso e erro. Isso reduz acoplamento no cliente e simplifica testes.

Resposta de sucesso

{  "data": { ... },  "meta": { ... }}

Resposta de erro (padronizada)

{  "error": {    "code": "validation_error",    "message": "Invalid input",    "details": [      { "field": "email", "issue": "invalid" }    ],    "traceId": "..."  }}

O traceId pode vir do logger/observabilidade e ajuda a correlacionar logs e erros.

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

Modelagem mínima: entidades e repositórios

Entidade User (domínio)

Mantenha a entidade com regras essenciais (ex.: e-mail obrigatório, status ativo/inativo). Evite colocar detalhes de HTTP/DB aqui.

final class User {    public function __construct(        public readonly string $id,        public string $name,        public string $email,        public string $passwordHash,        public bool $active,        public readonly DateTimeImmutable $createdAt    ) {} }

Interfaces de repositório (domínio)

interface UserRepository {    public function findById(string $id): ?User;    public function findByEmail(string $email): ?User;    /** @return array{items: User[], total: int} */    public function list(int $page, int $perPage): array;    public function create(User $user): void;    public function update(User $user): void; }

Retornar array{items,total} facilita paginação e testes, sem expor SQL.

Passo a passo: implementando os casos de uso

O fluxo recomendado é: DTO de entradaUseCaseRepository/ServicesDTO de saída. As Actions HTTP apenas adaptam Request/Response.

1) DTOs de entrada e saída

Crie DTOs simples para transportar dados já validados/sanitizados.

final class CreateUserInput {    public function __construct(        public readonly string $name,        public readonly string $email,        public readonly string $password    ) {} }final class UserOutput {    public function __construct(        public readonly string $id,        public readonly string $name,        public readonly string $email,        public readonly bool $active    ) {} }

2) UseCase: CreateUser

O caso de uso orquestra: verificar duplicidade, gerar hash, persistir e retornar saída. Ele não deve conhecer Slim nem detalhes de PDO.

final class CreateUser {    public function __construct(        private UserRepository $users,        private PasswordHasher $hasher,        private IdGenerator $ids,        private Clock $clock    ) {}    public function execute(CreateUserInput $in): UserOutput {        $existing = $this->users->findByEmail($in->email);        if ($existing) {            throw new DomainException('email_already_in_use');        }        $user = new User(            id: $this->ids->newId(),            name: $in->name,            email: $in->email,            passwordHash: $this->hasher->hash($in->password),            active: true,            createdAt: $this->clock->now()        );        $this->users->create($user);        return new UserOutput($user->id, $user->name, $user->email, $user->active);    } }

Ponto de qualidade: PasswordHasher, IdGenerator e Clock são abstrações para facilitar testes determinísticos.

3) UseCase: Login

O login valida credenciais e emite tokens via um serviço de segurança (infra). O caso de uso retorna um DTO com token e expiração.

final class LoginInput {    public function __construct(        public readonly string $email,        public readonly string $password    ) {} }final class TokenOutput {    public function __construct(        public readonly string $accessToken,        public readonly int $expiresIn    ) {} }final class Login {    public function __construct(        private UserRepository $users,        private PasswordHasher $hasher,        private TokenIssuer $tokens    ) {}    public function execute(LoginInput $in): TokenOutput {        $user = $this->users->findByEmail($in->email);        if (!$user || !$user->active) {            throw new DomainException('invalid_credentials');        }        if (!$this->hasher->verify($in->password, $user->passwordHash)) {            throw new DomainException('invalid_credentials');        }        return $this->tokens->issueForUser($user->id);    } }

HTTP: Actions enxutas + validação/sanitização

As Actions devem: ler o corpo/params, validar/sanitizar, montar DTO, chamar use case e serializar a resposta.

Action: POST /v1/users

final class CreateUserAction {    public function __construct(        private CreateUser $useCase,        private InputValidator $validator,        private JsonResponder $responder    ) {}    public function __invoke($request, $response): Psr\Http\Message\ResponseInterface {        $payload = (array)($request->getParsedBody() ?? []);        $data = $this->validator->validate($payload, [            'name' => ['required', 'string', 'min:2'],            'email' => ['required', 'email'],            'password' => ['required', 'string', 'min:8']        ]);        $out = $this->useCase->execute(new CreateUserInput(            name: $data['name'],            email: $data['email'],            password: $data['password']        ));        return $this->responder->created($response, ['data' => $out]);    } }

Observação: InputValidator representa o componente de validação/sanitização já adotado no projeto. A Action não deve conter regras de negócio.

Action: POST /v1/auth/login

final class LoginAction {    public function __construct(        private Login $useCase,        private InputValidator $validator,        private JsonResponder $responder    ) {}    public function __invoke($request, $response): Psr\Http\Message\ResponseInterface {        $payload = (array)($request->getParsedBody() ?? []);        $data = $this->validator->validate($payload, [            'email' => ['required', 'email'],            'password' => ['required', 'string']        ]);        $out = $this->useCase->execute(new LoginInput($data['email'], $data['password']));        return $this->responder->ok($response, ['data' => $out]);    } }

Rotas organizadas e versionadas

Centralize o registro em config/routes.php, agrupando por versão e domínio. Exemplo de desenho:

$app->group('/v1', function ($group) {    $group->group('/auth', function ($auth) {        $auth->post('/login', LoginAction::class);    });    $group->group('/users', function ($users) {        $users->post('', CreateUserAction::class);        $users->get('', ListUsersAction::class);        $users->get('/{id}', GetUserAction::class);        $users->put('/{id}', UpdateUserAction::class);        $users->delete('/{id}', DisableUserAction::class);    });});

Mantenha as rotas “finas”: sem closures com lógica, apenas apontando para Actions.

Middlewares aplicados no projeto (ordem e responsabilidade)

O comportamento final depende da ordem do pipeline. Um arranjo típico:

OrdemMiddlewareObjetivo
1CORSResponder preflight e anexar headers.
2RequestId/TraceGerar/propagar traceId.
3LoggingLogar request/response com contexto.
4ErrorHandlerConverter exceções em JSON padronizado.
5AuthProteger rotas específicas e injetar usuário no request.

Aplicando auth apenas onde precisa

Evite autenticar endpoints públicos (ex.: login). Aplique o middleware no grupo protegido:

$app->group('/v1', function ($group) {    $group->group('/auth', function ($auth) {        $auth->post('/login', LoginAction::class);    });    $group->group('/users', function ($users) {        $users->get('', ListUsersAction::class);        $users->get('/{id}', GetUserAction::class);        $users->put('/{id}', UpdateUserAction::class);        $users->delete('/{id}', DisableUserAction::class);    })->add(AuthMiddleware::class);});

O endpoint de criação de usuário pode ser público (cadastro) ou protegido (admin). Decida no requisito e aplique o middleware conforme necessário.

DI e composição: registrando dependências essenciais

Garanta que Actions recebam dependências por construtor. No container, registre interfaces para implementações concretas.

Exemplo de bindings (pseudo)

$container->set(UserRepository::class, function($c) {    return new PdoUserRepository($c->get(PDO::class));});$container->set(TokenIssuer::class, function($c) {    return new JwtTokenIssuer($c->get('settings')['jwt']);});$container->set(CreateUser::class, function($c) {    return new CreateUser(        $c->get(UserRepository::class),        $c->get(PasswordHasher::class),        $c->get(IdGenerator::class),        $c->get(Clock::class)    );});

Critério: Actions não devem criar repositórios/serviços com new. Isso preserva testabilidade e reduz acoplamento.

Persistência: implementação do repositório (infra)

Implemente o repositório com PDO (ou camada já definida) respeitando a interface do domínio. Exemplo reduzido:

final class PdoUserRepository implements UserRepository {    public function __construct(private PDO $pdo) {}    public function findByEmail(string $email): ?User {        $stmt = $this->pdo->prepare('SELECT * FROM users WHERE email = :email LIMIT 1');        $stmt->execute(['email' => $email]);        $row = $stmt->fetch(PDO::FETCH_ASSOC);        return $row ? $this->mapRowToUser($row) : null;    }    public function create(User $user): void {        $stmt = $this->pdo->prepare('INSERT INTO users (id,name,email,password_hash,active,created_at) VALUES (:id,:name,:email,:ph,:active,:ca)');        $stmt->execute([            'id' => $user->id,            'name' => $user->name,            'email' => $user->email,            'ph' => $user->passwordHash,            'active' => $user->active ? 1 : 0,            'ca' => $user->createdAt->format('Y-m-d H:i:s')        ]);    }    private function mapRowToUser(array $row): User {        return new User(            id: $row['id'],            name: $row['name'],            email: $row['email'],            passwordHash: $row['password_hash'],            active: (bool)$row['active'],            createdAt: new DateTimeImmutable($row['created_at'])        );    } }

Se um caso de uso exigir múltiplas operações atômicas, encapsule em transação na infraestrutura (ou via um TransactionManager injetado).

Tratamento de erros: mapeamento para HTTP

Padronize o mapeamento de exceções de domínio/aplicação para status HTTP e códigos de erro. Mantenha uma tabela simples e previsível.

Exceção/códigoHTTPcode
validation_error422validation_error
email_already_in_use409email_already_in_use
invalid_credentials401invalid_credentials
not_found404not_found
unexpected500internal_error

O middleware de erro deve capturar exceções, gerar traceId (ou reutilizar), logar e responder no formato padrão.

Checklist de qualidade (critérios de aceitação)

Arquitetura e separação de camadas

  • Actions não contêm regra de negócio; apenas adaptam HTTP → DTO → UseCase → resposta.
  • UseCases não dependem de Slim, Request/Response, nem de classes de infraestrutura.
  • Repositórios são interfaces no domínio e implementações na infraestrutura.
  • Serviços transversais (JWT, hashing, clock, logger) são abstraídos por interfaces quando impactam testes.

Consistência de API

  • Todas as respostas seguem o contrato {data, meta} ou {error}.
  • Erros de validação retornam 422 com detalhes por campo.
  • Erros de autenticação retornam 401; autorização (quando aplicável) 403.
  • Paginação retorna meta com page, perPage, total.

Testabilidade

  • UseCases testáveis com doubles de UserRepository, TokenIssuer, PasswordHasher.
  • Actions testáveis via testes HTTP (integração) sem acessar DB real (quando desejado) ou com DB de teste.
  • Configuração por ambiente permite apontar para banco/segredos diferentes em dev/test/prod.

Observabilidade e segurança

  • Logging não inclui senha, token ou dados sensíveis.
  • JWT secret e configs não ficam hardcoded no código.
  • CORS restritivo por ambiente (origens permitidas).
  • TraceId presente em logs e respostas de erro.

Passos para executar e testar o projeto

1) Variáveis de ambiente (exemplo)

Crie um arquivo .env (ou mecanismo equivalente) com:

APP_ENV=devDB_DSN=mysql:host=localhost;dbname=appDB_USER=rootDB_PASS=secretJWT_SECRET=change-meJWT_TTL=3600CORS_ALLOWED_ORIGINS=http://localhost:3000

2) Subir a aplicação

  • Instalar dependências: composer install
  • Rodar migrações/DDL do banco (conforme seu setup).
  • Iniciar servidor: php -S localhost:8080 -t public

3) Testar endpoints com curl

Criar usuário

curl -X POST http://localhost:8080/v1/users   -H 'Content-Type: application/json'   -d '{"name":"Ana","email":"ana@example.com","password":"secret123"}'

Login

curl -X POST http://localhost:8080/v1/auth/login   -H 'Content-Type: application/json'   -d '{"email":"ana@example.com","password":"secret123"}'

Acessar rota protegida

curl http://localhost:8080/v1/users   -H 'Authorization: Bearer <TOKEN>'

4) Testes automatizados (sugestão de organização)

  • Unitários: UseCases (CreateUser, Login) com repositório fake/mocked.
  • Integração: PdoUserRepository com banco de teste.
  • HTTP: rotas + middlewares + error handler garantindo contrato de resposta.

Como evoluir o projeto: adicionando novas funcionalidades sem quebrar a arquitetura

Exemplo A: endpoint de troca de senha

  • Rota: POST /v1/users/{id}/change-password (protegida).
  • DTO: ChangePasswordInput com currentPassword e newPassword.
  • UseCase: valida senha atual, aplica hash, atualiza via repositório.
  • Action: valida payload, cria DTO, chama use case, retorna 204 ou {data}.

Exemplo B: autorização por papéis (admin)

  • Adicionar claim role no token.
  • Criar middleware RequireRoleMiddleware('admin') aplicável a grupos específicos.
  • Manter a decisão de autorização fora das Actions, preferindo middleware/políticas.

Exemplo C: filtros e ordenação em listagem

  • Expandir ListUsersInput com search, sort, direction.
  • Atualizar repositório para aceitar critérios (sem expor SQL para a camada de aplicação).
  • Garantir que a validação de parâmetros de query aconteça na Action.

Roteiro de verificação rápida (antes de considerar “pronto”)

  • Endpoints respondem com JSON válido e headers corretos (incluindo CORS quando aplicável).
  • Erros de validação retornam 422 com details por campo.
  • Rotas protegidas retornam 401 sem token e 200/204 com token válido.
  • Logs possuem traceId e não vazam segredos.
  • UseCases não importam namespaces de HTTP/Slim.
  • Repositórios são substituíveis (interface no domínio).
  • Configuração muda por ambiente sem alterar código.

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

Ao aplicar Arquitetura Limpa em uma API com Slim, qual prática melhor preserva a separação entre HTTP, aplicação e infraestrutura?

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

Você errou! Tente novamente.

A separação correta mantém HTTP (Slim) como adaptação, enquanto a aplicação concentra a orquestração (UseCases) sem conhecer Slim. Repositórios ficam como interfaces no domínio e suas implementações na infraestrutura, preservando baixo acoplamento e testabilidade.

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

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.