Skip to content

Arquitetura de APIs

Resumo Inicial

Essa é a maneira padrão de organizar o funcionamento de APIs RESTful na nova Tech.


Visão Geral

Esta arquitetura segue o princípio de Separação de Responsabilidades (SoC), onde cada camada tem uma função específica e bem definida:

  • Controllers: Responsáveis pela validação de requisições e orquestração do fluxo
  • DTOs (Data Transfer Objects): Estruturas de dados tipadas para transferência de informações entre camadas
  • Actions: Responsáveis pela lógica de negócio e persistência de dados no banco de dados
  • API Resources: Responsáveis pela formatação e serialização dos dados de resposta
┌─────────────────────┐
│   API Controller    │ ← Valida requisição HTTP
└──────────┬──────────┘
           │ cria
┌─────────────────────┐
│        DTO          │ ← Transfere dados tipados
└──────────┬──────────┘
           │ passa para
┌─────────────────────┐
│       Action        │ ← Persiste no banco de dados
└──────────┬──────────┘
           │ retorna modelo
┌─────────────────────┐
│   API Resource      │ ← Formata resposta JSON
└─────────────────────┘

⚠️ ATENÇÃO - Versionamento de Rotas (OBRIGATÓRIO)

Sempre comece com versão v1 nas rotas da API, mesmo que seja a primeira versão. Isso é crítico para evitar problemas quando precisar fazer mudanças incompatíveis no futuro.

// ✅ CORRETO: Comece com v1
Route::prefix('v1')->group(function () {
    Route::apiResource('produtos', ProdutoController::class);
});
// Acesso: /api/v1/produtos

// ❌ ERRADO: Sem versão
Route::group(function () {
    Route::apiResource('produtos', ProdutoController::class);
});
// Acesso: /api/produtos (problema: não pode mudar sem quebrar clientes)

Por que é obrigatório?

  1. Mudanças futuras: Quando precisar mudar a resposta de um endpoint, crie /api/v2/ e mantenha /api/v1/ intacta
  2. Sem quebra de clientes: Aplicações antigas continuam funcionando
  3. Transição gradual: Migre clientes progressivamente
  4. Padrão de mercado: Todas as APIs profissionais usam versionamento

Veja mais detalhes na boas prática #10 (Versionamento de API).


Camadas da Arquitetura

1. API Controller (Camada de Apresentação)

Responsabilidades:

  • Receber requisições HTTP
  • Validar dados da requisição (via Form Request)
  • Criar instância do DTO
  • Chamar a Action apropriada
  • Retornar API Resource com resposta formatada
  • Documentar todos os endpoints com atributos PHP (para geração automática de Swagger)

NÃO deve:

  • ❌ Manipular diretamente o banco de dados
  • ❌ Conter lógica de negócio complexa
  • ❌ Retornar dados não tipados (sempre usar API Resources, inclusive em erros)
  • ❌ Fazer cálculos ou transformações de dados além da validação

2. DTO (Camada de Transferência)

Responsabilidades:

  • Encapsular dados em uma estrutura tipada
  • Garantir type safety entre camadas
    • Dados que podem possuir formatos variados devem utilizar uma Interface única.
    • Ex: datas devem ser tipadas como Carbon ao invés de apenas string
  • Facilitar refatoração e manutenção
  • Documentar a estrutura de dados esperada

Características:

  • Classes simples com propriedades públicas
  • Tipagem forte (PHP 8.x)
  • Sem lógica de negócio
  • Imutáveis após criação (quando possível)

3. Action (Camada de Negócio)

Responsabilidades:

  • Executar lógica de negócio
  • Persistir dados no banco de dados
  • Gerenciar transações
  • Lançar eventos quando necessário
  • Retornar resultado padronizado

Características:

  • Uma classe por operação (CreateX, UpdateX, DeleteX)
  • Método execute() como ponto de entrada
  • Uso de transactions quando necessário
  • Retorna array com success e dados relevantes

4. API Resource (Camada de Serialização)

Responsabilidades:

  • Formatar dados para resposta JSON
  • Controlar quais campos são expostos
  • Transformar tipos de dados (datas, números, etc)
  • Incluir relacionamentos quando necessário
  • Manter consistência no formato de resposta

Características:

  • Uma classe por modelo principal
  • Herda de JsonResource
  • Define estrutura clara de resposta
  • Pode incluir relacionamentos condicionalmente

Fluxo de Dados

<?php

// 1. Cliente faz requisição HTTP
// 2. Controller valida os dados (FormRequest) com atributos Swagger
public function store(CreateExemploRequest $request)
{
    // 3. Cria DTO com dados validados
    $dto = new CreateExemploDTO(
        campo1: $request->validated('campo1'),
        campo2: $request->validated('campo2'),
        created_by: auth()->id(),
    );

    // 4. Chama Action passando o DTO
    $action = new CreateExemplo();
    $resultado = $action->execute($dto);

    // 5. Trata o resultado
    if (!$resultado['success']) {
        // Sempre retorna tipado via ErrorResource
        return (new ErrorResource(
            message: 'Erro ao criar exemplo',
            exception: $resultado['exception'],
        ))->response()->setStatusCode(500);
    }

    // 6. Retorna API Resource formatado com status correto
    return (new ExemploResource($resultado['exemplo']))
        ->response()
        ->setStatusCode(201);
}

Componentes da Arquitetura

Error Resource

Localização: app/Http/Resources/

Estrutura básica:

Uma Resource especial para serializar erros de forma consistente:

<?php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
use OpenApi\Attributes as OA;

#[OA\Schema(
    schema: 'ErrorResource',
    properties: [
        new OA\Property(property: 'message', type: 'string', example: 'Erro ao processar requisição'),
        new OA\Property(property: 'error', type: 'string', example: 'Internal Server Error', description: 'Apenas em modo debug'),
        new OA\Property(property: 'status_code', type: 'integer', example: 500),
    ]
)]
class ErrorResource extends JsonResource
{
    public function __construct(
        public string $message,
        public ?\Throwable $exception = null,
    ) {
        parent::__construct(null);
    }

    public function toArray(Request $request): array
    {
        return [
            'message' => $this->message,
            'error' => config('app.debug') ? $this->exception?->getMessage() : null,
            'status_code' => $this->resource,
        ];
    }
}

Utilização:

Sempre retorne erros via ErrorResource para manter consistência na formatação:

1
2
3
4
5
6
7
8
<?php

if (!$resultado['success']) {
    return (new ErrorResource(
        message: 'Erro ao criar exemplo',
        exception: $resultado['exception'],
    ))->response()->setStatusCode(500);
}

Form Request

Localização: app/Http/Requests/Api/

Estrutura básica:

<?php

namespace App\Http\Requests\Api;

use Illuminate\Foundation\Http\FormRequest;

class CreateExemploRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'nome' => 'required|string|max:255',
            'categoria_id' => 'required|integer|exists:categorias,id',
            'data' => 'required|date',
            'observacoes' => 'nullable|string|max:1000',
        ];
    }

    public function messages(): array
    {
        return [
            'nome.required' => 'O nome é obrigatório',
            'categoria_id.required' => 'A categoria é obrigatória',
            'categoria_id.exists' => 'Categoria não encontrada',
        ];
    }
}

DTO (Data Transfer Object)

Localização: app/DTOs/

Estrutura básica:

<?php

namespace App\DTOs;

use App\Traits\DTOHelper;
use Carbon\Carbon;

class CreateExemploDTO
{
    use DTOHelper;

    public function __construct(
        public string $nome,
        public int $categoria_id,
        public Carbon $data,
        public string|null $observacoes = null,
        public int|null $created_by = null,
    ) {}
}

Action

Localização: app/Actions/

Estrutura básica:

<?php

namespace App\Actions\Exemplo;

use App\DTOs\CreateExemploDTO;
use App\Models\Exemplo;
use Illuminate\Support\Facades\DB;

class CreateExemplo
{
    public function execute(CreateExemploDTO $dto): array
    {
        return DB::transaction(function () use ($dto) {
            try {
                $exemplo = Exemplo::query()->create([
                    'nome' => $dto->nome,
                    'categoria_id' => $dto->categoria_id,
                    'data' => $dto->data,
                    'observacoes' => $dto->observacoes,
                    'created_by' => $dto->created_by ?? auth()->id(),
                ]);

                // Lógica adicional (ex: relacionamentos, históricos, etc)
                // ...

                return [
                    'success' => true,
                    'exemplo' => $exemplo,
                ];
            } catch (\Exception $e) {
                return [
                    'success' => false,
                    'exception' => $e,
                ];
            }
        });
    }
}

API Resource

Localização: app/Http/Resources/

Estrutura básica:

<?php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class ExemploResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'nome' => $this->nome,
            'categoria' => new CategoriaResource($this->whenLoaded('categoria')),
            'data' => $this->data->format('Y-m-d'),
            'observacoes' => $this->observacoes,
            'created_at' => $this->created_at->toIso8601String(),
            'updated_at' => $this->updated_at->toIso8601String(),
            'created_by' => new UserResource($this->whenLoaded('creator')),
        ];
    }
}

API Controller

Localização: app/Http/Controllers/Api/

Estrutura básica:

<?php

namespace App\Http\Controllers\Api;

use App\Actions\Exemplo\CreateExemplo;
use App\Actions\Exemplo\UpdateExemplo;
use App\Actions\Exemplo\DeleteExemplo;
use App\DTOs\CreateExemploDTO;
use App\DTOs\EditExemploDTO;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\CreateExemploRequest;
use App\Http\Requests\Api\UpdateExemploRequest;
use App\Http\Resources\ErrorResource;
use App\Http\Resources\ExemploResource;
use App\Models\Exemplo;
use Carbon\Carbon;
use OpenApi\Attributes as OA;

class ExemploController extends Controller
{
    #[OA\Get(
        path: '/api/exemplos',
        summary: 'Listar exemplos',
        tags: ['Exemplos'],
        parameters: [
            new OA\Parameter(
                name: 'page',
                in: 'query',
                description: 'Número da página',
                required: false,
                schema: new OA\Schema(type: 'integer', default: 1)
            ),
            new OA\Parameter(
                name: 'per_page',
                in: 'query',
                description: 'Itens por página',
                required: false,
                schema: new OA\Schema(type: 'integer', default: 15)
            ),
        ],
        responses: [
            new OA\Response(
                response: 200,
                description: 'Lista de exemplos',
                content: new OA\JsonContent(
                    properties: [
                        new OA\Property(
                            property: 'data',
                            type: 'array',
                            items: new OA\Items(ref: '#/components/schemas/ExemploResource')
                        ),
                        new OA\Property(property: 'links', type: 'object'),
                        new OA\Property(property: 'meta', type: 'object'),
                    ]
                )
            ),
            new OA\Response(response: 401, description: 'Não autenticado'),
            new OA\Response(response: 403, description: 'Não autorizado'),
        ]
    )]
    public function index()
    {
        $this->authorize('viewAny', Exemplo::class);

        $exemplos = Exemplo::query()
            ->with(['categoria', 'creator'])
            ->paginate(request('per_page', 15));

        return ExemploResource::collection($exemplos);
    }

    #[OA\Get(
        path: '/api/exemplos/{id}',
        summary: 'Exibir exemplo',
        tags: ['Exemplos'],
        parameters: [
            new OA\Parameter(
                name: 'id',
                in: 'path',
                description: 'ID do exemplo',
                required: true,
                schema: new OA\Schema(type: 'integer')
            ),
        ],
        responses: [
            new OA\Response(
                response: 200,
                description: 'Exemplo encontrado',
                content: new OA\JsonContent(ref: '#/components/schemas/ExemploResource')
            ),
            new OA\Response(response: 404, description: 'Exemplo não encontrado'),
            new OA\Response(response: 401, description: 'Não autenticado'),
            new OA\Response(response: 403, description: 'Não autorizado'),
        ]
    )]
    public function show(Exemplo $exemplo)
    {
        $this->authorize('view', $exemplo);

        $exemplo->load(['categoria', 'creator']);

        return new ExemploResource($exemplo);
    }

    #[OA\Post(
        path: '/api/exemplos',
        summary: 'Criar novo exemplo',
        tags: ['Exemplos'],
        requestBody: new OA\RequestBody(
            required: true,
            content: new OA\JsonContent(
                required: ['nome', 'categoria_id', 'data'],
                properties: [
                    new OA\Property(property: 'nome', type: 'string', maxLength: 255, example: 'Exemplo 1'),
                    new OA\Property(property: 'categoria_id', type: 'integer', example: 1),
                    new OA\Property(property: 'data', type: 'string', format: 'date', example: '2024-01-01'),
                    new OA\Property(property: 'observacoes', type: 'string', maxLength: 1000, example: 'Observações do exemplo'),
                ]
            )
        ),
        responses: [
            new OA\Response(
                response: 201,
                description: 'Exemplo criado com sucesso',
                content: new OA\JsonContent(ref: '#/components/schemas/ExemploResource')
            ),
            new OA\Response(response: 422, description: 'Erro de validação'),
            new OA\Response(response: 401, description: 'Não autenticado'),
            new OA\Response(response: 403, description: 'Não autorizado'),
        ]
    )]
    public function store(CreateExemploRequest $request)
    {
        // Validação já feita pelo FormRequest

        // Cria o DTO
        $dto = new CreateExemploDTO(
            nome: $request->validated('nome'),
            categoria_id: $request->validated('categoria_id'),
            data: Carbon::parse($request->validated('data')),
            observacoes: $request->validated('observacoes'),
            created_by: auth()->id(),
        );

        // Executa a Action
        $action = new CreateExemplo();
        $resultado = $action->execute($dto);

        // Trata o resultado
        if (!$resultado['success']) {
            return (new ErrorResource(
                message: 'Erro ao criar exemplo',
                exception: $resultado['exception'],
            ))->response()->setStatusCode(500);
        }

        // Retorna Resource com status 201
        return (new ExemploResource($resultado['exemplo']))
            ->response()
            ->setStatusCode(201);
    }

    #[OA\Put(
        path: '/api/exemplos/{id}',
        summary: 'Atualizar exemplo',
        tags: ['Exemplos'],
        parameters: [
            new OA\Parameter(
                name: 'id',
                in: 'path',
                description: 'ID do exemplo',
                required: true,
                schema: new OA\Schema(type: 'integer')
            ),
        ],
        requestBody: new OA\RequestBody(
            required: true,
            content: new OA\JsonContent(
                required: ['nome', 'categoria_id', 'data'],
                properties: [
                    new OA\Property(property: 'nome', type: 'string', maxLength: 255, example: 'Exemplo 1 Atualizado'),
                    new OA\Property(property: 'categoria_id', type: 'integer', example: 1),
                    new OA\Property(property: 'data', type: 'string', format: 'date', example: '2024-01-01'),
                    new OA\Property(property: 'observacoes', type: 'string', maxLength: 1000, example: 'Observações atualizadas'),
                ]
            )
        ),
        responses: [
            new OA\Response(
                response: 200,
                description: 'Exemplo atualizado com sucesso',
                content: new OA\JsonContent(ref: '#/components/schemas/ExemploResource')
            ),
            new OA\Response(response: 422, description: 'Erro de validação'),
            new OA\Response(response: 404, description: 'Exemplo não encontrado'),
            new OA\Response(response: 401, description: 'Não autenticado'),
            new OA\Response(response: 403, description: 'Não autorizado'),
        ]
    )]
    public function update(UpdateExemploRequest $request, Exemplo $exemplo)
    {
        // Validação já feita pelo FormRequest

        // Cria o DTO
        $dto = new EditExemploDTO(
            id: $exemplo->id,
            nome: $request->validated('nome'),
            categoria_id: $request->validated('categoria_id'),
            data: Carbon::parse($request->validated('data')),
            observacoes: $request->validated('observacoes'),
            updated_by: auth()->id(),
        );

        // Executa a Action
        $action = new UpdateExemplo();
        $resultado = $action->execute($dto);

        // Trata o resultado
        if (!$resultado['success']) {
            return (new ErrorResource(
                message: 'Erro ao atualizar exemplo',
                exception: $resultado['exception'],
            ))->response()->setStatusCode(500);
        }

        // Retorna Resource
        return new ExemploResource($resultado['exemplo']);
    }

    #[OA\Delete(
        path: '/api/exemplos/{id}',
        summary: 'Deletar exemplo',
        tags: ['Exemplos'],
        parameters: [
            new OA\Parameter(
                name: 'id',
                in: 'path',
                description: 'ID do exemplo',
                required: true,
                schema: new OA\Schema(type: 'integer')
            ),
        ],
        responses: [
            new OA\Response(
                response: 204,
                description: 'Exemplo deletado com sucesso'
            ),
            new OA\Response(response: 404, description: 'Exemplo não encontrado'),
            new OA\Response(response: 401, description: 'Não autenticado'),
            new OA\Response(response: 403, description: 'Não autorizado'),
        ]
    )]
    public function destroy(Exemplo $exemplo)
    {
        $this->authorize('delete', $exemplo);

        // Executa a Action
        $action = new DeleteExemplo();
        $resultado = $action->execute($exemplo->id);

        // Trata o resultado
        if (!$resultado['success']) {
            return (new ErrorResource(
                message: 'Erro ao deletar exemplo',
                exception: $resultado['exception'],
            ))->response()->setStatusCode(500);
        }

        // Retorna 204 No Content
        return response()->noContent();
    }
}

Implementação Passo a Passo

Passo 1: Criar o Model e Migration

php artisan make:model Exemplo -m

Passo 2: Criar Form Requests

php artisan make:request Api/CreateExemploRequest
php artisan make:request Api/UpdateExemploRequest

Edite os arquivos gerados:

CreateExemploRequest:

<?php

namespace App\Http\Requests\Api;

use App\Models\Exemplo;
use Illuminate\Foundation\Http\FormRequest;

class CreateExemploRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'nome' => 'required|string|max:255',
            'categoria_id' => 'required|integer|exists:categorias,id',
            'data' => 'required|date',
            'observacoes' => 'nullable|string|max:1000',
        ];
    }
}

UpdateExemploRequest:

<?php

namespace App\Http\Requests\Api;

use App\Models\Exemplo;
use Illuminate\Foundation\Http\FormRequest;

class UpdateExemploRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'nome' => 'required|string|max:255',
            'categoria_id' => 'required|integer|exists:categorias,id',
            'data' => 'required|date',
            'observacoes' => 'nullable|string|max:1000',
        ];
    }
}

Passo 3: Criar DTOs

php artisan make:dto CreateExemploDTO
php artisan make:dto EditExemploDTO

CreateExemploDTO:

<?php

namespace App\DTOs;

use App\Traits\DTOHelper;
use Carbon\Carbon;

class CreateExemploDTO
{
    use DTOHelper;

    public function __construct(
        public string $nome,
        public int $categoria_id,
        public Carbon $data,
        public ?string $observacoes = null,
        public ?int $created_by = null,
    ) {}
}

EditExemploDTO:

<?php

namespace App\DTOs;

use App\Traits\DTOHelper;
use Carbon\Carbon;

class EditExemploDTO
{
    use DTOHelper;

    public function __construct(
        public int $id,
        public string $nome,
        public int $categoria_id,
        public Carbon $data,
        public ?string $observacoes = null,
        public ?int $updated_by = null,
    ) {}
}

Passo 4: Criar Actions

1
2
3
php artisan make:action Exemplo/CreateExemplo
php artisan make:action Exemplo/UpdateExemplo
php artisan make:action Exemplo/DeleteExemplo

CreateExemplo:

<?php

namespace App\Actions\Exemplo;

use App\DTOs\CreateExemploDTO;
use App\Models\Exemplo;
use Illuminate\Support\Facades\DB;

class CreateExemplo
{
    public function execute(CreateExemploDTO $dto): array
    {
        return DB::transaction(function () use ($dto) {
            try {
                $exemplo = Exemplo::query()->create([
                    'nome' => $dto->nome,
                    'categoria_id' => $dto->categoria_id,
                    'data' => $dto->data,
                    'observacoes' => $dto->observacoes,
                    'created_by' => $dto->created_by ?? auth()->id(),
                ]);

                return [
                    'success' => true,
                    'exemplo' => $exemplo,
                ];
            } catch (\Exception $e) {
                return [
                    'success' => false,
                    'exception' => $e,
                ];
            }
        });
    }
}

UpdateExemplo:

<?php

namespace App\Actions\Exemplo;

use App\DTOs\EditExemploDTO;
use App\Models\Exemplo;
use Illuminate\Support\Facades\DB;

class UpdateExemplo
{
    public function execute(EditExemploDTO $dto): array
    {
        return DB::transaction(function () use ($dto) {
            try {
                $exemplo = Exemplo::query()->findOrFail($dto->id);

                $exemplo->update([
                    'nome' => $dto->nome,
                    'categoria_id' => $dto->categoria_id,
                    'data' => $dto->data,
                    'observacoes' => $dto->observacoes,
                    'updated_by' => $dto->updated_by ?? auth()->id(),
                ]);

                return [
                    'success' => true,
                    'exemplo' => $exemplo->fresh(),
                ];
            } catch (\Exception $e) {
                return [
                    'success' => false,
                    'exception' => $e,
                ];
            }
        });
    }
}

DeleteExemplo:

<?php

namespace App\Actions\Exemplo;

use App\Models\Exemplo;
use Illuminate\Support\Facades\DB;

class DeleteExemplo
{
    public function execute(int $id): array
    {
        return DB::transaction(function () use ($id) {
            try {
                $exemplo = Exemplo::query()->findOrFail($id);
                $exemplo->delete();

                return ['success' => true];
            } catch (\Exception $e) {
                return [
                    'success' => false,
                    'exception' => $e,
                ];
            }
        });
    }
}

Passo 5: Criar API Resource

php artisan make:resource ExemploResource

Edite o arquivo gerado:

<?php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
use OpenApi\Attributes as OA;

#[OA\Schema(
    schema: 'ExemploResource',
    properties: [
        new OA\Property(property: 'id', type: 'integer', example: 1),
        new OA\Property(property: 'nome', type: 'string', example: 'Exemplo 1'),
        new OA\Property(property: 'categoria', ref: '#/components/schemas/CategoriaResource'),
        new OA\Property(property: 'data', type: 'string', format: 'date', example: '2024-01-01'),
        new OA\Property(property: 'observacoes', type: 'string', example: 'Observações do exemplo'),
        new OA\Property(property: 'created_at', type: 'string', format: 'date-time'),
        new OA\Property(property: 'updated_at', type: 'string', format: 'date-time'),
        new OA\Property(property: 'created_by', ref: '#/components/schemas/UserResource'),
    ]
)]
class ExemploResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'nome' => $this->nome,
            'categoria' => new CategoriaResource($this->whenLoaded('categoria')),
            'data' => $this->data->format('Y-m-d'),
            'observacoes' => $this->observacoes,
            'created_at' => $this->created_at->toIso8601String(),
            'updated_at' => $this->updated_at->toIso8601String(),
            'created_by' => new UserResource($this->whenLoaded('creator')),
        ];
    }
}

Passo 6: Criar Controller

php artisan make:controller Api/ExemploController --api

Implemente os métodos conforme exemplo mostrado anteriormente.

Passo 7: Registrar Rotas

Em routes/api.php:

1
2
3
4
5
6
7
8
<?php

use App\Http\Controllers\Api\ExemploController;
use Illuminate\Support\Facades\Route;

Route::prefix('v1')->middleware(['auth:sanctum'])->group(function () {
    Route::apiResource('exemplos', ExemploController::class);
});

⚠️ ATENÇÃO: Note o prefix('v1') - SEMPRE versione suas rotas desde o início, mesmo que seja v1!

Isso garante que você possa criar /api/v2/ no futuro sem quebrar clientes existentes em /api/v1/.


Boas Práticas

1. Sempre Use API Resources (Inclusive para Erros)

Errado:

<?php

public function show(Exemplo $exemplo)
{
    return response()->json($exemplo);
}

public function store(Request $request)
{
    // ...
    if ($erro) {
        return response()->json(['message' => 'Erro'], 500);
    }
}

Correto:

<?php

public function show(Exemplo $exemplo)
{
    return new ExemploResource($exemplo);
}

public function store(CreateExemploRequest $request)
{
    // ...
    if (!$resultado['success']) {
        return (new ErrorResource(
            message: 'Erro ao criar exemplo',
            exception: $resultado['exception'],
        ))->response()->setStatusCode(500);
    }

    return (new ExemploResource($resultado['exemplo']))
        ->response()
        ->setStatusCode(201);
}

2. Documente Todos os Endpoints com Atributos PHP

Todo método do Controller deve ter documentação Swagger usando atributos PHP. A documentação deve incluir:

  • Descrição: O que o endpoint faz
  • Parâmetros: Path, query e request body
  • Responses: Sucesso e todos os possíveis erros
  • Tags: Para organização no Swagger
<?php

#[OA\Post(
    path: '/api/produtos',
    summary: 'Criar novo produto',
    tags: ['Produtos'],
    requestBody: new OA\RequestBody(
        required: true,
        content: new OA\JsonContent(
            required: ['nome', 'preco'],
            properties: [
                new OA\Property(property: 'nome', type: 'string', example: 'Produto'),
                new OA\Property(property: 'preco', type: 'number', example: 99.90),
            ]
        )
    ),
    responses: [
        new OA\Response(
            response: 201,
            description: 'Produto criado',
            content: new OA\JsonContent(ref: '#/components/schemas/ProdutoResource')
        ),
        new OA\Response(response: 422, description: 'Erro de validação'),
        new OA\Response(response: 500, description: 'Erro interno'),
    ]
)]
public function store(CreateProdutoRequest $request)
{
    // ...
}

3. Use Form Requests para Validação

Errado:

1
2
3
4
5
6
7
8
<?php

public function store(Request $request)
{
    $validated = $request->validate([
        'nome' => 'required|string',
    ]);
}

Correto:

1
2
3
4
5
6
7
<?php

public function store(CreateExemploRequest $request)
{
    // Validação já feita
    $validated = $request->validated();
}

4. DTOs Específicos para Create e Update

<?php

// CreateExemploDTO.php
public function __construct(
    public string $nome,
    public int $categoria_id,
    public ?int $created_by = null,
) {}

// EditExemploDTO.php
public function __construct(
    public int $id,
    public string $nome,
    public int $categoria_id,
    public ?int $updated_by = null,
) {}

5. Status HTTP Corretos

<?php

// 200 - OK (padrão para GET, PUT)
return new ExemploResource($exemplo);

// 201 - Created (para POST)
return (new ExemploResource($exemplo))
    ->response()
    ->setStatusCode(201);

// 204 - No Content (para DELETE)
return response()->noContent();

// 422 - Unprocessable Entity (validação)
// Automático ao usar FormRequest

// 500 - Internal Server Error (sempre via ErrorResource)
return (new ErrorResource(
    message: 'Erro ao processar requisição',
    exception: $e,
))->response()->setStatusCode(500);

6. Eager Loading em Relacionamentos

<?php

public function index()
{
    $exemplos = Exemplo::query()
        ->with(['categoria', 'creator']) // Evita N+1 queries
        ->paginate(15);

    return ExemploResource::collection($exemplos);
}

7. Paginação em Listagens

1
2
3
4
5
6
7
8
9
<?php

public function index()
{
    $exemplos = Exemplo::query()
        ->paginate(request('per_page', 15));

    return ExemploResource::collection($exemplos);
}

8. Autorização com Policies

<?php

public function store(CreateExemploRequest $request)
{
    // Autorização feita no FormRequest
}

public function show(Exemplo $exemplo)
{
    $this->authorize('view', $exemplo);

    return new ExemploResource($exemplo);
}

9. Tratamento de Erros Consistente

Sempre use ErrorResource para retornar erros de forma tipada e consistente:

1
2
3
4
5
6
7
8
<?php

if (!$resultado['success']) {
    return (new ErrorResource(
        message: 'Mensagem amigável para o usuário',
        exception: $resultado['exception'],
    ))->response()->setStatusCode(500);
}

10. Versionamento de API

⚠️ IMPORTANTE - IMPLEMENTE DESDE O INÍCIO:

Sempre versione suas rotas de API, mesmo em versão 1. Isso evitará quebras futuras quando precisar fazer mudanças incompatíveis.

<?php

// routes/api.php
Route::prefix('v1')->group(function () {
    Route::apiResource('exemplos', ExemploController::class);
    Route::apiResource('produtos', ProdutoController::class);
    // ... outras rotas v1
});

// Acesso: /api/v1/exemplos
// Acesso: /api/v1/produtos

Por que versionar?

  1. Compatibilidade com clientes: Clientes antigos continuam funcionando
  2. Mudanças sem quebrar: Você pode criar /api/v2/ sem afetar /api/v1/
  3. Transição gradual: Migre clientes progressivamente de uma versão para outra
  4. Documentação clara: Fica evidente qual versão está em uso

Cenário de migração (futuro):

<?php

// routes/api.php
Route::prefix('v1')->group(function () {
    Route::apiResource('exemplos', ExemploController::class);
    // Versão 1 permanece intacta
});

Route::prefix('v2')->group(function () {
    Route::apiResource('exemplos', ExemploV2Controller::class);
    // Nova implementação em v2
    // Mudanças incompatíveis apenas aqui
});

// Clientes podem:
// - Usar /api/v1/exemplos (mantém comportamento antigo)
// - Usar /api/v2/exemplos (novo comportamento)

Exemplo de resposta do header (para informar versão):

1
2
3
4
5
6
<?php

// No Controller, você pode adicionar:
return (new ExemploResource($exemplo))
    ->response()
    ->header('X-API-Version', 'v1');

Exemplos Práticos

Exemplo 1: CRUD Completo de Produtos

Form Requests:

<?php

// CreateProdutoRequest.php
class CreateProdutoRequest extends FormRequest
{
    public function authorize(): bool
    {
        return $this->user()->can('criar', Produto::class);
    }

    public function rules(): array
    {
        return [
            'nome' => 'required|string|max:255',
            'descricao' => 'nullable|string|max:1000',
            'preco' => 'required|numeric|min:0',
            'categoria_id' => 'required|integer|exists:categorias,id',
            'ativo' => 'boolean',
        ];
    }
}

// UpdateProdutoRequest.php
class UpdateProdutoRequest extends FormRequest
{
    public function authorize(): bool
    {
        return $this->user()->can('editar', $this->route('produto'));
    }

    public function rules(): array
    {
        return [
            'nome' => 'required|string|max:255',
            'descricao' => 'nullable|string|max:1000',
            'preco' => 'required|numeric|min:0',
            'categoria_id' => 'required|integer|exists:categorias,id',
            'ativo' => 'boolean',
        ];
    }
}

DTOs:

<?php

// CreateProdutoDTO.php
class CreateProdutoDTO
{
    use DTOHelper;

    public function __construct(
        public string $nome,
        public float $preco,
        public int $categoria_id,
        public ?string $descricao = null,
        public bool $ativo = true,
        public ?int $created_by = null,
    ) {}
}

// EditProdutoDTO.php
class EditProdutoDTO
{
    use DTOHelper;

    public function __construct(
        public int $id,
        public string $nome,
        public float $preco,
        public int $categoria_id,
        public ?string $descricao = null,
        public bool $ativo = true,
        public ?int $updated_by = null,
    ) {}
}

Actions:

<?php

// CreateProduto.php
class CreateProduto
{
    public function execute(CreateProdutoDTO $dto): array
    {
        return DB::transaction(function () use ($dto) {
            try {
                $produto = Produto::query()->create([
                    'nome' => $dto->nome,
                    'descricao' => $dto->descricao,
                    'preco' => $dto->preco,
                    'categoria_id' => $dto->categoria_id,
                    'ativo' => $dto->ativo,
                    'created_by' => $dto->created_by ?? auth()->id(),
                ]);

                return [
                    'success' => true,
                    'produto' => $produto,
                ];
            } catch (\Exception $e) {
                return [
                    'success' => false,
                    'exception' => $e,
                ];
            }
        });
    }
}

// UpdateProduto.php
class UpdateProduto
{
    public function execute(EditProdutoDTO $dto): array
    {
        return DB::transaction(function () use ($dto) {
            try {
                $produto = Produto::query()->findOrFail($dto->id);

                $produto->update([
                    'nome' => $dto->nome,
                    'descricao' => $dto->descricao,
                    'preco' => $dto->preco,
                    'categoria_id' => $dto->categoria_id,
                    'ativo' => $dto->ativo,
                    'updated_by' => $dto->updated_by ?? auth()->id(),
                ]);

                return [
                    'success' => true,
                    'produto' => $produto->fresh(),
                ];
            } catch (\Exception $e) {
                return [
                    'success' => false,
                    'exception' => $e,
                ];
            }
        });
    }
}

API Resource:

<?php

#[OA\Schema(
    schema: 'ProdutoResource',
    properties: [
        new OA\Property(property: 'id', type: 'integer'),
        new OA\Property(property: 'nome', type: 'string'),
        new OA\Property(property: 'descricao', type: 'string'),
        new OA\Property(property: 'preco', type: 'number', format: 'float'),
        new OA\Property(property: 'ativo', type: 'boolean'),
        new OA\Property(property: 'categoria', ref: '#/components/schemas/CategoriaResource'),
        new OA\Property(property: 'created_at', type: 'string', format: 'date-time'),
        new OA\Property(property: 'updated_at', type: 'string', format: 'date-time'),
    ]
)]
class ProdutoResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'nome' => $this->nome,
            'descricao' => $this->descricao,
            'preco' => number_format($this->preco, 2, '.', ''),
            'ativo' => (bool) $this->ativo,
            'categoria' => new CategoriaResource($this->whenLoaded('categoria')),
            'created_at' => $this->created_at->toIso8601String(),
            'updated_at' => $this->updated_at->toIso8601String(),
        ];
    }
}

Controller:

<?php

class ProdutoController extends Controller
{
    #[OA\Post(
        path: '/api/produtos',
        summary: 'Criar novo produto',
        tags: ['Produtos'],
        requestBody: new OA\RequestBody(
            required: true,
            content: new OA\JsonContent(
                required: ['nome', 'preco', 'categoria_id'],
                properties: [
                    new OA\Property(property: 'nome', type: 'string', example: 'Produto Exemplo'),
                    new OA\Property(property: 'descricao', type: 'string', example: 'Descrição do produto'),
                    new OA\Property(property: 'preco', type: 'number', format: 'float', example: 99.90),
                    new OA\Property(property: 'categoria_id', type: 'integer', example: 1),
                    new OA\Property(property: 'ativo', type: 'boolean', example: true),
                ]
            )
        ),
        responses: [
            new OA\Response(
                response: 201,
                description: 'Produto criado com sucesso',
                content: new OA\JsonContent(ref: '#/components/schemas/ProdutoResource')
            ),
        ]
    )]
    public function store(CreateProdutoRequest $request)
    {
        $dto = new CreateProdutoDTO(
            nome: $request->validated('nome'),
            preco: $request->validated('preco'),
            categoria_id: $request->validated('categoria_id'),
            descricao: $request->validated('descricao'),
            ativo: $request->validated('ativo', true),
            created_by: auth()->id(),
        );

        $action = new CreateProduto();
        $resultado = $action->execute($dto);

        if (!$resultado['success']) {
            return (new ErrorResource(
                message: 'Erro ao criar produto',
                exception: $resultado['exception'],
            ))->response()->setStatusCode(500);
        }

        return (new ProdutoResource($resultado['produto']))
            ->response()
            ->setStatusCode(201);
    }
}

Exemplo 2: API com Relacionamentos Complexos

DTO:

<?php

class CreatePedidoDTO
{
    use DTOHelper;

    public function __construct(
        public int $cliente_id,
        public Carbon $data_pedido,
        public array $itens, // [['produto_id' => 1, 'quantidade' => 2], ...]
        public ?string $observacoes = null,
        public ?int $created_by = null,
    ) {}
}

Action:

<?php

class CreatePedido
{
    public function execute(CreatePedidoDTO $dto): array
    {
        return DB::transaction(function () use ($dto) {
            try {
                // Cria o pedido
                $pedido = Pedido::query()->create([
                    'cliente_id' => $dto->cliente_id,
                    'data_pedido' => $dto->data_pedido,
                    'observacoes' => $dto->observacoes,
                    'created_by' => $dto->created_by ?? auth()->id(),
                ]);

                // Cria os itens do pedido
                $total = 0;
                foreach ($dto->itens as $item) {
                    $produto = Produto::query()->findOrFail($item['produto_id']);

                    $subtotal = $produto->preco * $item['quantidade'];
                    $total += $subtotal;

                    PedidoItem::query()->create([
                        'pedido_id' => $pedido->id,
                        'produto_id' => $item['produto_id'],
                        'quantidade' => $item['quantidade'],
                        'preco_unitario' => $produto->preco,
                        'subtotal' => $subtotal,
                    ]);
                }

                // Atualiza o total do pedido
                $pedido->update(['total' => $total]);

                return [
                    'success' => true,
                    'pedido' => $pedido->load(['cliente', 'itens.produto']),
                ];
            } catch (\Exception $e) {
                return [
                    'success' => false,
                    'exception' => $e,
                ];
            }
        });
    }
}

Resource:

<?php

class PedidoResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'cliente' => new ClienteResource($this->whenLoaded('cliente')),
            'data_pedido' => $this->data_pedido->format('Y-m-d'),
            'total' => number_format($this->total, 2, '.', ''),
            'observacoes' => $this->observacoes,
            'itens' => PedidoItemResource::collection($this->whenLoaded('itens')),
            'created_at' => $this->created_at->toIso8601String(),
        ];
    }
}

Exemplo 3: Filtros e Busca

Controller:

<?php

#[OA\Get(
    path: '/api/produtos',
    summary: 'Listar produtos com filtros',
    tags: ['Produtos'],
    parameters: [
        new OA\Parameter(
            name: 'categoria_id',
            in: 'query',
            description: 'Filtrar por categoria',
            schema: new OA\Schema(type: 'integer')
        ),
        new OA\Parameter(
            name: 'search',
            in: 'query',
            description: 'Buscar por nome ou descrição',
            schema: new OA\Schema(type: 'string')
        ),
        new OA\Parameter(
            name: 'ativo',
            in: 'query',
            description: 'Filtrar por status',
            schema: new OA\Schema(type: 'boolean')
        ),
        new OA\Parameter(
            name: 'preco_min',
            in: 'query',
            description: 'Preço mínimo',
            schema: new OA\Schema(type: 'number')
        ),
        new OA\Parameter(
            name: 'preco_max',
            in: 'query',
            description: 'Preço máximo',
            schema: new OA\Schema(type: 'number')
        ),
    ],
    responses: [
        new OA\Response(
            response: 200,
            description: 'Lista de produtos',
        ),
    ]
)]
public function index(Request $request)
{
    $query = Produto::query()->with(['categoria']);

    // Filtro por categoria
    if ($request->filled('categoria_id')) {
        $query->where('categoria_id', $request->categoria_id);
    }

    // Busca por nome ou descrição
    if ($request->filled('search')) {
        $query->where(function ($q) use ($request) {
            $q->where('nome', 'like', '%' . $request->search . '%')
              ->orWhere('descricao', 'like', '%' . $request->search . '%');
        });
    }

    // Filtro por status
    if ($request->filled('ativo')) {
        $query->where('ativo', $request->boolean('ativo'));
    }

    // Filtro por faixa de preço
    if ($request->filled('preco_min')) {
        $query->where('preco', '>=', $request->preco_min);
    }
    if ($request->filled('preco_max')) {
        $query->where('preco', '<=', $request->preco_max);
    }

    $produtos = $query->paginate($request->input('per_page', 15));

    return ProdutoResource::collection($produtos);
}


Vantagens da Arquitetura

1. Separação de Responsabilidades

  • Controllers apenas orquestram
  • Actions contêm lógica de negócio
  • Resources formatam respostas
  • Form Requests validam dados

2. Type Safety

  • DTOs garantem tipos corretos
  • Resources definem estrutura de resposta
  • Menos erros em produção

3. Documentação Automática

  • Atributos PHP geram Swagger automaticamente
  • Documentação sempre atualizada
  • Facilita integração com front-end

4. Reutilização de Código

  • Actions podem ser usadas em diferentes contextos
  • Resources podem ser reutilizados
  • DTOs facilitam conversões

5. Testabilidade

  • Cada camada pode ser testada isoladamente
  • Mocks facilitados por injeção de dependência
<?php

public function test_create_produto()
{
    $dto = new CreateProdutoDTO(
        nome: 'Teste',
        preco: 100.00,
        categoria_id: 1,
    );

    $action = new CreateProduto();
    $resultado = $action->execute($dto);

    $this->assertTrue($resultado['success']);
}

6. Versionamento Facilitado

  • Fácil criar v2 da API
  • Resources permitem formatos diferentes

7. Consistência

  • Todas as respostas seguem mesmo padrão
  • Erros tratados de forma uniforme

Conclusão

Esta arquitetura oferece:

APIs bem estruturadas e documentadas
Respostas tipadas e consistentes (inclusive erros)
Código reutilizável e testável
Documentação Swagger automática via atributos PHP (obrigatória em todos endpoints)
Fácil manutenção e evolução
Type safety em toda aplicação
ErrorResource para tratamento consistente de erros

Ao seguir este padrão, você garante que as APIs do projeto W2O Safety sejam profissionais, bem documentadas, com erros tipados e fáceis de manter.

⚠️ IMPORTANTE: Todos os métodos do Controller DEVEM ter documentação Swagger usando atributos PHP. A documentação não é opcional, é parte essencial da arquitetura de API.