Как правильно организовать API в Laravel: лучшие практики и примеры

Раздел: Laravel -> Создание API на Laravel

Создание API на Laravel: структура и основные подходы

Основное эффективное решение: использование API Resources, Form Requests и Sanctum

Наиболее современный и поддерживаемый способ построения API в Laravel базируется на трёх компонентах: API Resources для трансформации данных, Form Requests для валидации и Laravel Sanctum для аутентификации. Такой подход обеспечивает чистый код, автоматическую документацию (через Scribe) и простую поддержку.

Как создать простой RESTful API для модели Article с использованием Resources и Form Requests?


// 1. Модель и миграция
php artisan make:model Article -m

// 2. Создание Resource
php artisan make:resource ArticleResource

// 3. Создание ResourceCollection
php artisan make:resource ArticleCollection

// 4. Создание FormRequest
php artisan make:request StoreArticleRequest
php artisan make:request UpdateArticleRequest

// 5. Контроллер
php artisan make:controller Api/ArticleController --api

Api php laravel (api в laravel php)


// app/Http/Resources/ArticleResource.php
<?php
namespace App\Http\Resources;

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

class ArticleResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
            'body' => $this->body,
            'created_at' => $this->created_at->format('Y-m-d H:i:s'),
            'author' => new UserResource($this->whenLoaded('author')),
        ];
    }
}

// app/Http/Requests/StoreArticleRequest.php
<?php
namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StoreArticleRequest extends FormRequest
{
    public function authorize(): bool
    {
        return $this->user()->can('create', Article::class);
    }

    public function rules(): array
    {
        return [
            'title' => 'required|string|max:255',
            'body' => 'required|string|min:10',
        ];
    }
}

// routes/api.php
Route::middleware('auth:sanctum')->apiResource('articles', ArticleController::class);

// app/Http/Controllers/Api/ArticleController.php (фрагмент)
public function index()
{
    $articles = Article::with('author')->paginate(15);
    return new ArticleCollection($articles);
}

public function store(StoreArticleRequest $request)
{
    $article = $request->user()->articles()->create($request->validated());
    return new ArticleResource($article);
}

Типичные ошибки и их решение

  • Ошибка 500 при отсутствии отношения – если в Resource используется whenLoaded для отношения, которого нет в выборке. Решение: всегда загружать отношение через with() или проверять $this->relationLoaded().
  • Проблемы с CORS – при запросах с другого домена. Решение: установить пакет fruitcake/laravel-cors (встроен в Laravel 9+) и настроить конфиг cors.php.
  • Ошибка аутентификации 401 – если Sanctum не настроен правильно. Решение: проверить APP_KEY, миграцию таблицы personal_access_tokens, и использовать заголовок Authorization: Bearer {token}.

Вариант 1: Использование только API Resources без Form Requests

Когда валидация минимальна или выполняется на стороне модели (через правила в модели), можно не создавать отдельные FormRequest, а валидировать прямо в контроллере с помощью $request->validate(). Этот подход подходит для очень маленьких проектов или прототипов.


public function store(Request $request)
{
    $validated = $request->validate([
        'title' => 'required|string|max:255',
        'body' => 'required|string|min:10',
    ]);

    $article = Auth::user()->articles()->create($validated);
    return new ArticleResource($article);
}

Проблемы

При росте количества методов валидация дублируется, правила трудно переиспользовать. Решение: выносить валидацию в отдельные классы.

Вариант 2: Ручное формирование JSON-ответов без Resources

Для максимального контроля (например, при интеграции с legacy-системами) можно возвращать массивы или использовать response()->json(). Но такой код тяжело поддерживать и тестировать.


public function index()
{
    $articles = Article::with('author')->get();
    $data = $articles->map(fn($article) => [
        'id' => $article->id,
        'title' => $article->title,
        'author_name' => $article->author->name,
    ]);
    return response()->json(['data' => $data]);
}

Типичная ошибка

Забывают обрабатывать пустые коллекции или отсутствующие отношения. Решение: использовать optional() или проверять null.

Вариант 3: Использование Fractal (легаси)

Пакет league/fractal ранее был популярен для трансформации данных. В Laravel 11 он считается устаревшим, но может встречаться в старых проектах. Пример трансформера:


use League\Fractal\TransformerAbstract;

class ArticleTransformer extends TransformerAbstract
{
    public function transform(Article $article): array
    {
        return [
            'id' => $article->id,
            'title' => $article->title,
        ];
    }
}

// в контроллере
$resource = new Collection($articles, new ArticleTransformer());
$data = $this->fractal->createData($resource)->toArray();

Проблемы

Дополнительная зависимость, необходимость управлять фракталом через фасад или внедрение зависимости. Решение: мигрировать на встроенные API Resources.

Вариант 4: Аутентификация через Laravel Passport

Для полноценного OAuth2 (например, когда API будут использовать сторонние приложения) вместо Sanctum можно применить Passport. Он предоставляет полноценные токены доступа и рефреш-токены.


composer require laravel/passport
php artisan passport:install

// в AuthServiceProvider::boot()
Passport::routes();

// в модели User
use Laravel\Passport\HasApiTokens;

// маршруты
Route::middleware('auth:api')->group(function () {
    // ...
});

Типичные ошибки

  • Не запущены миграции Passport – решение: php artisan migrate.
  • Не настроен драйвер аутентификации api в config/auth.php.
  • Срок действия токена истекает – настраивается в Passport::tokensExpireIn().

Вариант 5: Версионирование API через маршруты и заголовки

Часто требуется поддерживать несколько версий API. Популярные способы: префиксы в URL (/api/v1/articles) или заголовок Accept: application/vnd.myapi.v1+json. В Laravel это реализуется через группы маршрутов.


Route::prefix('v1')->group(function () {
    Route::apiResource('articles', V1\ArticleController::class);
});

Route::prefix('v2')->group(function () {
    Route::apiResource('articles', V2\ArticleController::class);
});

Проблемы

Дублирование кода контроллеров. Решение: использовать наследование или сервисный слой.

Расширенные примеры реализации API на Laravel

Пример 1: Полный REST API для статей с пагинацией, фильтрацией и вложенными ресурсами

Допустим, необходимо предоставить эндпоинты для статей с возможностью фильтрации по категории, сортировки по дате и включения автора. Используем пакет spatie/laravel-query-builder для удобной фильтрации и сортировки.

Пример

// Установка пакета
composer require spatie/laravel-query-builder

// ArticleController
use Spatie\QueryBuilder\QueryBuilder;

public function index(Request $request)
{
    $articles = QueryBuilder::for(Article::class)
        ->allowedFilters(['title', 'category_id', 'created_at'])
        ->allowedSorts('created_at', 'title')
        ->allowedIncludes('author', 'comments')
        ->paginate($request->input('per_page', 15));

    return ArticleResource::collection($articles);
}
Пример

// Пример запроса
GET /api/v1/articles?filter[category_id]=2&sort=-created_at&include=author&per_page=5
Пример

// ArticleResource с поддержкой included
class ArticleResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
            'body' => $this->body,
            'created_at' => $this->created_at,
            'category' => new CategoryResource($this->whenLoaded('category')),
            'author' => new UserResource($this->whenLoaded('author')),
            'comments_count' => $this->whenCounted('comments'),
        ];
    }
}
// Результат для запроса выше (сокращён)
{
  "data": [
    {
      "id": 10,
      "title": "API Best Practices",
      "body": "Lorem ipsum...",
      "created_at": "2025-03-21T12:00:00.000000Z",
      "author": {
        "id": 1,
        "name": "Иван Петров"
      },
      "comments_count": 3
    }
  ],
  "meta": {
    "current_page": 1,
    "last_page": 4,
    "per_page": 5,
    "total": 20
  },
  "links": {
    "first": "http://example.com/api/v1/articles?page=1",
    "last": "http://example.com/api/v1/articles?page=4",
    "prev": null,
    "next": "http://example.com/api/v1/articles?page=2"
  }
}

Пример 2: Обработка ошибок и кастомные ответы

Для единообразного формата ошибок используется исключения Laravel и собственные классы ответов.

Пример

// app/Exceptions/Handler.php (Laravel 11)
public function register(): void
{
    $this->reportable(function (Throwable $e) {
        //
    });

    $this->renderable(function (ValidationException $e, $request) {
        if ($request->expectsJson()) {
            return response()->json([
                'message' => 'Ошибка валидации',
                'errors' => $e->errors(),
            ], 422);
        }
    });

    $this->renderable(function (ModelNotFoundException $e, $request) {
        if ($request->expectsJson()) {
            return response()->json([
                'message' => 'Ресурс не найден',
            ], 404);
        }
    });
}
Пример

// Использование в контроллере
use App\Exceptions\CustomApiException;

public function show($id)
{
    $article = Article::find($id);
    if (!$article) {
        throw new CustomApiException('Статья не найдена', 404);
    }
    return new ArticleResource($article);
}

Пример 3: Генерация API-токенов через Sanctum с ограничением способностей (abilities)

Пример

// В контроллере аутентификации
public function login(Request $request)
{
    $credentials = $request->validate([
        'email' => 'required|email',
        'password' => 'required',
    ]);

    if (!Auth::attempt($credentials)) {
        return response()->json(['message' => 'Неверные учётные данные'], 401);
    }

    $user = Auth::user();
    $token = $user->createToken('api-token', ['articles:read', 'articles:write'])->plainTextToken;

    return response()->json([
        'token' => $token,
        'abilities' => ['articles:read', 'articles:write'],
    ]);
}
Пример

// Использование в маршрутах
Route::middleware('auth:sanctum', 'abilities:articles:read')->get('/articles', [ArticleController::class, 'index']);
Route::middleware('auth:sanctum', 'abilities:articles:write')->post('/articles', [ArticleController::class, 'store']);

Пример 4: Тестирование API с использованием Laravel Dusk или PHPUnit

Пример

// tests/Feature/Api/ArticleTest.php
<?php

namespace Tests\Feature\Api;

use Tests\TestCase;
use App\Models\Article;
use App\Models\User;
use Laravel\Sanctum\Sanctum;

class ArticleTest extends TestCase
{
    public function test_can_list_articles()
    {
        Article::factory()->count(3)->create();

        $response = $this->getJson('/api/v1/articles');

        $response->assertStatus(200);
        $response->assertJsonCount(3, 'data');
    }

    public function test_unauthenticated_user_cannot_create_article()
    {
        $response = $this->postJson('/api/v1/articles', [
            'title' => 'Test',
            'body' => 'Content',
        ]);

        $response->assertStatus(401);
    }

    public function test_authenticated_user_can_create_article()
    {
        Sanctum::actingAs(
            User::factory()->create(),
            ['articles:write']
        );

        $response = $this->postJson('/api/v1/articles', [
            'title' => 'New Article',
            'body' => 'This is the body of the article',
        ]);

        $response->assertStatus(201);
        $response->assertJson([
            'data' => [
                'title' => 'New Article',
            ]
        ]);
    }
}
// Результат запуска php artisan test
PASS  Tests\Feature\Api\ArticleTest
✓ can list articles
✓ unauthenticated user cannot create article
✓ authenticated user can create article
Tests: 3 passed

Пример 5: Использование Rate Limiting для API

Пример

// В AppServiceProvider или RouteServiceProvider
RateLimiter::for('api', function (Request $request) {
    return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});

// В routes/api.php
Route::middleware('throttle:api')->group(function () {
    Route::apiResource('articles', ArticleController::class);
});
// При превышении лимита клиент получает ответ
{
  "message": "Too Many Attempts.",
  "retry_after": 30
}

API в Laravel PHP - comments

En
Api php laravel (php)