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.