Как правильно организовать 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
}