Projeto final: API completa com Laravel, testes e deploy
1. Planejamento e Estruturação da API
Antes de escrever uma linha de código, é fundamental definir os recursos da API. Para este projeto, construiremos uma API de blog com três entidades principais: Usuários, Posts e Categorias. As rotas RESTful seguirão o padrão:
GET /api/v1/posts— listar postsPOST /api/v1/posts— criar postGET /api/v1/posts/{id}— exibir postPUT /api/v1/posts/{id}— atualizar postDELETE /api/v1/posts/{id}— remover post
O mesmo padrão se aplica a usuários e categorias, com relacionamentos: um usuário tem muitos posts (hasMany), e um post pertence a muitas categorias (belongsToMany).
A modelagem do banco de dados começa com as migrations:
// database/migrations/xxxx_create_posts_table.php
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->string('title');
$table->text('body');
$table->timestamps();
});
// database/migrations/xxxx_create_category_post_table.php
Schema::create('category_post', function (Blueprint $table) {
$table->id();
$table->foreignId('category_id')->constrained();
$table->foreignId('post_id')->constrained();
$table->timestamps();
});
Para o ambiente de desenvolvimento, utilizaremos Laravel Sail, que fornece Docker pré-configurado com PHP, MySQL e Redis. Basta executar:
composer require laravel/sail --dev
php artisan sail:install
./vendor/bin/sail up
2. Implementação dos Recursos Principais
Com a estrutura definida, criamos os Models, Controllers e Factories. Vamos gerar tudo com Artisan:
php artisan make:model Post -mfc
php artisan make:model Category -mfc
php artisan make:model User -mfc
A autenticação será feita com Laravel Sanctum, que fornece tokens de API simples e seguros. Instale e configure:
composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate
No model User, adicione o trait HasApiTokens:
<?php
// app/Models/User.php
namespace App\Models;
use Laravel\Sanctum\HasApiTokens;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
use HasApiTokens;
// ...
}
A validação de dados será feita com Form Requests personalizados. Exemplo para criação de posts:
<?php
// app/Http/Requests/StorePostRequest.php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StorePostRequest extends FormRequest
{
public function authorize(): bool
{
return true; // A autorização será testada separadamente
}
public function rules(): array
{
return [
'title' => 'required|string|max:255',
'body' => 'required|string',
'categories' => 'required|array',
'categories.*' => 'exists:categories,id',
];
}
}
No controller, usamos o request personalizado:
<?php
// app/Http/Controllers/Api/V1/PostController.php
namespace App\Http\Controllers\Api\V1;
use App\Http\Requests\StorePostRequest;
use App\Models\Post;
use App\Http\Resources\PostResource;
class PostController extends Controller
{
public function store(StorePostRequest $request)
{
$post = auth()->user()->posts()->create($request->validated());
$post->categories()->sync($request->categories);
return new PostResource($post);
}
}
3. Relacionamentos e Recursos Avançados
Os relacionamentos Eloquent são configurados nos models:
// app/Models/User.php
public function posts(): HasMany
{
return $this->hasMany(Post::class);
}
// app/Models/Post.php
public function categories(): BelongsToMany
{
return $this->belongsToMany(Category::class);
}
Para transformar as respostas, criamos API Resources:
<?php
// app/Http/Resources/PostResource.php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class PostResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'body' => $this->body,
'author' => new UserResource($this->whenLoaded('user')),
'categories' => CategoryResource::collection($this->whenLoaded('categories')),
'created_at' => $this->created_at,
];
}
}
Para paginação, filtros e ordenação, implementamos no controller:
public function index(Request $request)
{
$query = Post::with(['user', 'categories']);
if ($request->has('category')) {
$query->whereHas('categories', fn($q) => $q->where('slug', $request->category));
}
$sortField = $request->get('sort', 'created_at');
$sortDir = $request->get('direction', 'desc');
$query->orderBy($sortField, $sortDir);
return PostResource::collection($query->paginate(15));
}
4. Testes Automatizados
Testes são essenciais para garantir que a API funcione corretamente. Usamos PHPUnit com Laravel Testing. Exemplo de teste de unidade para um model:
<?php
// tests/Unit/PostTest.php
namespace Tests\Unit;
use Tests\TestCase;
use App\Models\Post;
class PostTest extends TestCase
{
public function test_post_belongs_to_user()
{
$post = Post::factory()->create();
$this->assertInstanceOf(User::class, $post->user);
}
}
Teste de funcionalidade para endpoint:
<?php
// tests/Feature/PostApiTest.php
namespace Tests\Feature;
use Tests\TestCase;
use App\Models\User;
use App\Models\Post;
use Laravel\Sanctum\Sanctum;
class PostApiTest extends TestCase
{
public function test_authenticated_user_can_create_post()
{
Sanctum::actingAs(User::factory()->create());
$response = $this->postJson('/api/v1/posts', [
'title' => 'Novo Post',
'body' => 'Conteúdo do post',
'categories' => [1, 2]
]);
$response->assertStatus(201)
->assertJsonStructure(['data' => ['id', 'title']]);
}
public function test_unauthenticated_user_cannot_create_post()
{
$response = $this->postJson('/api/v1/posts', [
'title' => 'Teste',
'body' => 'Conteúdo'
]);
$response->assertStatus(401);
}
}
Testes de autorização e validação:
public function test_post_requires_title()
{
Sanctum::actingAs(User::factory()->create());
$response = $this->postJson('/api/v1/posts', [
'body' => 'Conteúdo sem título'
]);
$response->assertStatus(422)
->assertJsonValidationErrors('title');
}
5. Documentação e Versionamento
Para documentar a API, usamos Scribe, que gera documentação OpenAPI automaticamente:
composer require --dev knuckleswtf/scribe
php artisan scribe:generate
A documentação será gerada em public/docs/. Versionamos a API usando prefixo v1 nas rotas:
// routes/api.php
Route::prefix('v1')->group(function () {
Route::apiResource('posts', PostController::class);
Route::apiResource('categories', CategoryController::class);
Route::apiResource('users', UserController::class);
});
Para tratamento global de exceções, personalizamos o Handler:
// app/Exceptions/Handler.php
public function render($request, Throwable $e)
{
if ($request->expectsJson()) {
return response()->json([
'message' => $e->getMessage(),
], $e instanceof ModelNotFoundException ? 404 : 500);
}
return parent::render($request, $e);
}
6. Configuração para Deploy
Antes do deploy, preparamos o ambiente:
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan optimize
Configuramos o servidor web com Nginx + PHP-FPM. Exemplo de configuração:
server {
listen 80;
server_name api.exemplo.com;
root /var/www/api/public;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
Para conteinerização, criamos um Dockerfile:
FROM php:8.2-fpm
RUN apt-get update && apt-get install -y \
git unzip libpq-dev \
&& docker-php-ext-install pdo_mysql
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
WORKDIR /var/www
COPY . .
RUN composer install --optimize-autoloader --no-dev
RUN chown -R www-data:www-data storage bootstrap/cache
7. Deploy Contínuo e Monitoramento
Implementamos um pipeline CI/CD com GitHub Actions:
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
- name: Install dependencies
run: composer install --no-progress
- name: Run tests
run: php artisan test
deploy:
needs: test
runs-on: ubuntu-latest
steps:
- name: Deploy to DigitalOcean
uses: appleboy/ssh-action@v0.1.5
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
key: ${{ secrets.SSH_KEY }}
script: |
cd /var/www/api
git pull origin main
composer install --optimize-autoloader --no-dev
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan queue:restart
Para monitoramento, configuramos health checks no Laravel:
// routes/api.php
Route::get('/health', function () {
try {
DB::connection()->getPdo();
return response()->json(['status' => 'healthy']);
} catch (\Exception $e) {
return response()->json(['status' => 'unhealthy'], 500);
}
});
Logs podem ser visualizados com tail -f storage/logs/laravel.log e monitorados com serviços como Laravel Horizon para filas ou Sentry para erros.
Referências
- Documentação oficial do Laravel — Guia completo sobre todos os recursos do framework, incluindo Eloquent, Sanctum e testes.
- Laravel Sanctum — Documentação específica sobre autenticação com tokens de API.
- Laravel Sail — Ambiente de desenvolvimento Dockerizado para Laravel.
- Scribe para Laravel — Geração automática de documentação OpenAPI para APIs Laravel.
- GitHub Actions para Laravel — Guia oficial de deploy contínuo com GitHub Actions.
- Laravel Forge — Serviço de gerenciamento de servidores otimizado para Laravel, com deploy automático.
- Testes no Laravel — Documentação oficial sobre testes de unidade e funcionalidade com PHPUnit.