Backend-разработка

Проектирование REST API на Laravel: принципы, версионирование и документирование в 2026 году

Ruslan Ismailov Опубликовано 11 мин чтения
П

Введение: почему проектирование API важнее его реализации

Большинство разработчиков сначала пишут код, а потом задумываются об архитектуре. С REST API такой подход особенно разрушителен: неудачные решения на этапе проектирования превращаются в технический долг, который почти невозможно выплатить без breaking changes.

В 2026 году Laravel остаётся одним из самых популярных PHP-фреймворков для создания API-бэкендов. Его экосистема зрелая, инструментарий богатый — но это не отменяет необходимости думать перед тем, как писать. Эта статья — о том, как спроектировать REST API на Laravel правильно с первого раза.

Принципы RESTful-проектирования

Ресурсы и именование маршрутов

REST строится вокруг ресурсов, а не действий. URL должен обозначать существительное, а HTTP-метод — глагол.

  • Правильно: GET /api/v1/articles
  • Неправильно: GET /api/v1/getArticles

Используйте множественное число для коллекций (/users), вложенные маршруты для отношений (/users/42/posts), и избегайте глубокой вложенности более двух уровней — это усложняет клиентский код.

HTTP-методы и их семантика

  • GET — получение ресурса или коллекции (безопасный, идемпотентный)
  • POST — создание нового ресурса
  • PUT — полное обновление ресурса (идемпотентный)
  • PATCH — частичное обновление
  • DELETE — удаление ресурса (идемпотентный)

HTTP-статус-коды

Правильное использование статус-кодов — признак зрелого API. Не возвращайте 200 OK на каждый запрос с текстом ошибки в теле.

  • 200 — успешный GET/PUT/PATCH
  • 201 — успешный POST (ресурс создан)
  • 204 — успешный DELETE (тело пустое)
  • 400 — ошибка валидации
  • 401 — не аутентифицирован
  • 403 — нет прав доступа
  • 404 — ресурс не найден
  • 422 — Unprocessable Entity (Laravel использует это для валидации)
  • 500 — внутренняя ошибка сервера

Идемпотентность

Идемпотентный запрос при повторном выполнении даёт тот же результат. PUT /users/1 с теми же данными не должен создавать дубликаты. Это критично для надёжности при нестабильной сети.

Структура Laravel-проекта для API

Маршруты (Routes)

Все API-маршруты располагаются в routes/api.php. Laravel автоматически добавляет префикс /api и применяет middleware api.

// routes/api.php
use App\Http\Controllers\Api\V1\ArticleController;

Route::prefix('v1')->middleware('auth:sanctum')->group(function () {
    Route::apiResource('articles', ArticleController::class);
    Route::apiResource('users.posts', PostController::class)->shallow();
});

Контроллеры (Controllers)

Используйте --api флаг при генерации контроллеров — он создаёт только нужные методы без create и edit.

php artisan make:controller Api/V1/ArticleController --api --model=Article

Пример контроллера с использованием API Resources:

<?php

namespace App\Http\Controllers\Api\V1;

use App\Http\Controllers\Controller;
use App\Http\Requests\StoreArticleRequest;
use App\Http\Requests\UpdateArticleRequest;
use App\Http\Resources\ArticleResource;
use App\Models\Article;

class ArticleController extends Controller
{
    public function index()
    {
        $articles = Article::with('author')
            ->latest()
            ->paginate(15);

        return ArticleResource::collection($articles);
    }

    public function store(StoreArticleRequest $request)
    {
        $article = Article::create($request->validated());

        return new ArticleResource($article);
    }

    public function show(Article $article)
    {
        return new ArticleResource($article->load('author', 'tags'));
    }

    public function update(UpdateArticleRequest $request, Article $article)
    {
        $article->update($request->validated());

        return new ArticleResource($article);
    }

    public function destroy(Article $article)
    {
        $article->delete();

        return response()->noContent();
    }
}

API Resources

Никогда не возвращайте Eloquent-модели напрямую. API Resources — уровень трансформации данных, который защищает от случайной утечки полей и позволяет менять структуру ответа независимо от модели.

<?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,
            'slug'       => $this->slug,
            'content'    => $this->content,
            'published'  => $this->published_at?->toIso8601String(),
            'author'     => new UserResource($this->whenLoaded('author')),
            'tags'       => TagResource::collection($this->whenLoaded('tags')),
            'created_at' => $this->created_at->toIso8601String(),
        ];
    }
}

Form Requests

Выносите валидацию в Form Requests — это разгружает контроллеры и позволяет переиспользовать правила:

<?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'],
            'content' => ['required', 'string'],
            'tags'    => ['array'],
            'tags.*'  => ['integer', 'exists:tags,id'],
        ];
    }
}

Версионирование API: URI versioning vs Header versioning

Один из самых спорных вопросов в проектировании API. Существует два основных подхода:

URI Versioning

GET /api/v1/articles
GET /api/v2/articles

Плюсы: наглядно, легко тестировать в браузере, понятно для кэширования на уровне CDN. Минусы: URL технически не должен содержать информацию о версии протокола.

Header Versioning

GET /api/articles
Accept: application/vnd.myapp.v2+json

Плюсы: «чище» с точки зрения REST-пуризма. Минусы: сложнее в отладке, плохо работает с браузерными инструментами.

Рекомендация 2026 года: используйте URI versioning для публичных и партнёрских API — это прагматичный выбор, который выбирает подавляющее большинство крупных компаний (GitHub, Stripe, Twilio). Header versioning оправдан только если вы управляете всеми клиентами и можете гарантировать корректную передачу заголовков.

В Laravel версионирование через URI организуется структурой папок:

app/Http/Controllers/Api/V1/ArticleController.php
app/Http/Controllers/Api/V2/ArticleController.php

routes/api/v1.php
routes/api/v2.php

Аутентификация и авторизация: Laravel Sanctum vs Passport в 2026 году

Laravel Sanctum

Sanctum — рекомендуемое решение для большинства проектов в 2026 году. Поддерживает API-токены и SPA-аутентификацию через cookie-сессии. Прост в настройке, не требует OAuth-сервера.

// Установка
composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate

// Выдача токена
$token = $user->createToken('mobile-app', ['articles:read', 'articles:write']);
return ['token' => $token->plainTextToken];

// Проверка способностей токена
if ($request->user()->tokenCan('articles:write')) {
    // ...
}

Laravel Passport

Passport реализует полноценный OAuth 2.0-сервер. Нужен когда: вы выдаёте токены сторонним приложениям, требуется Authorization Code Flow, или вы строите платформу с API для партнёров.

Итог: если у вас мобильное приложение или SPA — используйте Sanctum. Если строите OAuth-провайдер для сторонних клиентов — Passport.

Обработка ошибок и стандартизация ответов

Единый формат ответов критичен для клиентских разработчиков. Настройте глобальный обработчик исключений в app/Exceptions/Handler.php:

<?php

use Illuminate\Auth\AuthenticationException;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\HttpException;

// В методе register():
$this->renderable(function (\Throwable $e, $request) {
    if ($request->expectsJson()) {
        if ($e instanceof ValidationException) {
            return response()->json([
                'message' => 'Validation failed',
                'errors'  => $e->errors(),
            ], 422);
        }

        if ($e instanceof AuthenticationException) {
            return response()->json([
                'message' => 'Unauthenticated.',
            ], 401);
        }

        if ($e instanceof HttpException) {
            return response()->json([
                'message' => $e->getMessage() ?: 'HTTP error',
            ], $e->getStatusCode());
        }

        return response()->json([
            'message' => 'Server error',
        ], 500);
    }
});

Стандартный успешный ответ с пагинацией Laravel автоматически включает поля data, links и meta при использовании Resource::collection() с пагинированием — используйте это.

Документирование API с помощью Scribe или L5-Swagger

Scribe

Scribe — современный инструмент автогенерации документации для Laravel. Анализирует маршруты, Form Requests и docblock-комментарии, генерируя красивую HTML-документацию и OpenAPI-спецификацию.

composer require --dev knuckleswtf/scribe
php artisan vendor:publish --tag=scribe-config
php artisan scribe:generate

Аннотируйте контроллеры для лучшей документации:

/**
 * @group Articles
 *
 * API для работы со статьями
 */
class ArticleController extends Controller
{
    /**
     * Список статей
     *
     * Возвращает пагинированный список всех опубликованных статей.
     *
     * @queryParam page integer Номер страницы. Example: 1
     * @queryParam per_page integer Количество на странице (макс. 50). Example: 15
     */
    public function index() { ... }
}

L5-Swagger (darkaonline/l5-swagger)

Если команда предпочитает OpenAPI 3.0 и Swagger UI, L5-Swagger остаётся хорошим выбором. Используйте PHP-атрибуты вместо аннотаций в комментариях — это типобезопасно и поддерживается IDE.

composer require darkaonline/l5-swagger
php artisan vendor:publish --provider "L5Swagger\L5SwaggerServiceProvider"
php artisan l5-swagger:generate

Совет: интегрируйте генерацию документации в CI/CD-пайплайн, чтобы документация всегда соответствовала коду.

Тестирование REST API в Laravel

Хорошо протестированный API — это API, которому доверяют. Laravel предоставляет мощные инструменты для Feature-тестов прямо из коробки.

<?php

namespace Tests\Feature\Api\V1;

use App\Models\Article;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class ArticleApiTest extends TestCase
{
    use RefreshDatabase;

    public function test_authenticated_user_can_create_article(): void
    {
        $user = User::factory()->create();

        $response = $this->actingAs($user, 'sanctum')
            ->postJson('/api/v1/articles', [
                'title'   => 'Test Article',
                'content' => 'Some content here',
            ]);

        $response
            ->assertStatus(201)
            ->assertJsonStructure([
                'data' => ['id', 'title', 'slug', 'content', 'created_at'],
            ])
            ->assertJsonPath('data.title', 'Test Article');

        $this->assertDatabaseHas('articles', ['title' => 'Test Article']);
    }

    public function test_unauthenticated_request_returns_401(): void
    {
        $this->postJson('/api/v1/articles', ['title' => 'Test'])
            ->assertStatus(401);
    }

    public function test_validation_returns_422_with_errors(): void
    {
        $user = User::factory()->create();

        $this->actingAs($user, 'sanctum')
            ->postJson('/api/v1/articles', [])
            ->assertStatus(422)
            ->assertJsonValidationErrors(['title', 'content']);
    }

    public function test_article_list_is_paginated(): void
    {
        $user = User::factory()->create();
        Article::factory()->count(20)->create();

        $this->actingAs($user, 'sanctum')
            ->getJson('/api/v1/articles')
            ->assertStatus(200)
            ->assertJsonStructure([
                'data', 'links', 'meta' => ['total', 'per_page', 'current_page'],
            ]);
    }
}

Запускайте тесты с флагом --parallel для ускорения в CI:

php artisan test --parallel --coverage

Заключение: чек-лист готового API

Перед тем как считать API готовым к production, пройдитесь по этому чек-листу:

  1. Маршруты: используют существительные во множественном числе, версионированы через URI (/api/v1/)
  2. HTTP-методы: используются по семантике — GET, POST, PUT/PATCH, DELETE
  3. Статус-коды: 201 на создание, 204 на удаление, 422 на валидацию, 401/403 на auth-ошибки
  4. API Resources: все ответы проходят через Resource-трансформеры, модели не возвращаются напрямую
  5. Form Requests: валидация и авторизация вынесены из контроллеров
  6. Аутентификация: настроен Sanctum (или Passport для OAuth) с правильными scope/abilities
  7. Обработка ошибок: глобальный handler возвращает JSON-ответы для всех исключений
  8. Версионирование: маршруты и контроллеры структурированы по версиям
  9. Документация: сгенерирована через Scribe или L5-Swagger, интегрирована в CI
  10. Тесты: покрыты happy path, валидационные ошибки и auth-сценарии
  11. Rate Limiting: настроен через RateLimiter::for() в RouteServiceProvider
  12. N+1 проблемы: устранены через eager loading, подключён Laravel Debugbar или Telescope для мониторинга

Качественное REST API на Laravel — это не только правильный код, но и продуманная архитектура, предсказуемое поведение и хорошая документация. Инвестируйте время в проектирование — это окупится многократно при масштабировании и командной разработке.

Технологии

Теги

Руслан Исмаилов

Senior Web / Backend разработчик. Senior web/backend разработчик с 9-летним опытом. Стек: PHP, Laravel, PostgreSQL, Redis, Docker, Kubernetes, REST, микросервисы, CI/CD. Подробнее обо мне →