← Назад к вопросам
REST API для авторов и книг
2.2 Middle🔥 191 комментариев
#API и веб-протоколы#Тестирование#Фреймворки
Условие
Создать REST API для управления авторами и книгами с регистрацией пользователей.
Сущности
Автор:
- id, имя, дата рождения, биография
Книга:
- id, название, год издания, ISBN, author_id
API эндпоинты
- POST /api/register - регистрация
- POST /api/login - авторизация
- GET/POST/PUT/DELETE /api/authors - CRUD авторов
- GET/POST/PUT/DELETE /api/books - CRUD книг
- GET /api/authors/{id}/books - книги автора
Требования
- Аутентификация через Laravel Sanctum
- Валидация данных
- Пагинация списков
- Resource классы для ответов API
- Покрытие тестами 90%+
Технологии
Laravel 10+, MySQL, Laravel Sanctum, PHPUnit
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Решение
1. Миграции
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void {
// Таблица авторов
Schema::create("authors", function (Blueprint $table) {
$table->id();
$table->string("name", 255);
$table->date("birth_date")->nullable();
$table->text("biography")->nullable();
$table->timestamps();
$table->index("name");
});
// Таблица книг
Schema::create("books", function (Blueprint $table) {
$table->id();
$table->foreignId("author_id")->constrained("authors")->cascadeOnDelete();
$table->string("title", 255);
$table->year("publication_year")->nullable();
$table->string("isbn", 20)->unique()->nullable();
$table->text("description")->nullable();
$table->timestamps();
$table->index("author_id");
$table->index("title");
$table->index("isbn");
});
}
public function down(): void {
Schema::dropIfExists("books");
Schema::dropIfExists("authors");
}
};
2. Модели
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Author extends Model {
protected $fillable = ["name", "birth_date", "biography"];
protected $casts = ["birth_date" => "date"];
public function books(): HasMany {
return $this->hasMany(Book::class);
}
}
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Book extends Model {
protected $fillable = ["author_id", "title", "publication_year", "isbn", "description"];
protected $casts = ["publication_year" => "integer"];
public function author(): BelongsTo {
return $this->belongsTo(Author::class);
}
}
<?php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable {
use HasApiTokens, Notifiable;
protected $fillable = ["name", "email", "password"];
protected $hidden = ["password", "remember_token"];
protected $casts = ["email_verified_at" => "datetime"];
}
3. API Resources
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class AuthorResource extends JsonResource {
public function toArray($request) {
return [
"id" => $this->id,
"name" => $this->name,
"birth_date" => $this->birth_date,
"biography" => $this->biography,
"created_at" => $this->created_at,
"updated_at" => $this->updated_at,
];
}
}
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class BookResource extends JsonResource {
public function toArray($request) {
return [
"id" => $this->id,
"title" => $this->title,
"isbn" => $this->isbn,
"publication_year" => $this->publication_year,
"description" => $this->description,
"author" => new AuthorResource($this->whenLoaded("author")),
"author_id" => $this->author_id,
"created_at" => $this->created_at,
"updated_at" => $this->updated_at,
];
}
}
4. Form Requests для валидации
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class RegisterRequest extends FormRequest {
public function authorize(): bool { return true; }
public function rules(): array {
return [
"name" => "required|string|max:255",
"email" => "required|email|unique:users",
"password" => "required|min:8|confirmed",
];
}
}
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class LoginRequest extends FormRequest {
public function authorize(): bool { return true; }
public function rules(): array {
return [
"email" => "required|email",
"password" => "required|min:8",
];
}
}
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreAuthorRequest extends FormRequest {
public function authorize(): bool { return true; }
public function rules(): array {
return [
"name" => "required|string|max:255",
"birth_date" => "nullable|date",
"biography" => "nullable|string|max:2000",
];
}
}
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreBookRequest extends FormRequest {
public function authorize(): bool { return true; }
public function rules(): array {
return [
"author_id" => "required|exists:authors,id",
"title" => "required|string|max:255",
"publication_year" => "nullable|integer|min:1000|max:" . date("Y"),
"isbn" => "nullable|unique:books,isbn|regex:/^[0-9\-]+$/",
"description" => "nullable|string|max:5000",
];
}
}
5. API контроллеры
<?php
namespace App\Http\Controllers\Api;
use App\Http\Requests\LoginRequest;
use App\Http\Requests\RegisterRequest;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Hash;
class AuthController extends Controller {
public function register(RegisterRequest $request): JsonResponse {
$user = User::create([
"name" => $request->validated()["name"],
"email" => $request->validated()["email"],
"password" => Hash::make($request->validated()["password"]),
]);
$token = $user->createToken("api-token")->plainTextToken;
return response()->json([
"message" => "User registered successfully",
"user" => $user,
"token" => $token,
], 201);
}
public function login(LoginRequest $request): JsonResponse {
$user = User::where("email", $request->validated()["email"])->first();
if (!$user || !Hash::check($request->validated()["password"], $user->password)) {
return response()->json(["message" => "Invalid credentials"], 401);
}
$token = $user->createToken("api-token")->plainTextToken;
return response()->json([
"message" => "Login successful",
"user" => $user,
"token" => $token,
]);
}
public function logout(): JsonResponse {
auth()->user()->currentAccessToken()->delete();
return response()->json(["message" => "Logged out successfully"]);
}
}
<?php
namespace App\Http\Controllers\Api;
use App\Http\Requests\StoreAuthorRequest;
use App\Http\Resources\AuthorResource;
use App\Http\Resources\BookResource;
use App\Models\Author;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class AuthorController extends Controller {
public function index(Request $request): JsonResponse {
$authors = Author::paginate(20);
return response()->json(AuthorResource::collection($authors));
}
public function store(StoreAuthorRequest $request): JsonResponse {
$author = Author::create($request->validated());
return response()->json(new AuthorResource($author), 201);
}
public function show(Author $author): JsonResponse {
return response()->json(new AuthorResource($author));
}
public function update(StoreAuthorRequest $request, Author $author): JsonResponse {
$author->update($request->validated());
return response()->json(new AuthorResource($author));
}
public function destroy(Author $author): JsonResponse {
$author->delete();
return response()->json(["message" => "Author deleted successfully"]);
}
public function books(Author $author): JsonResponse {
$books = $author->books()->paginate(20);
return response()->json(BookResource::collection($books));
}
}
<?php
namespace App\Http\Controllers\Api;
use App\Http\Requests\StoreBookRequest;
use App\Http\Resources\BookResource;
use App\Models\Book;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class BookController extends Controller {
public function index(Request $request): JsonResponse {
$query = Book::with("author");
if ($request->has("author_id")) {
$query->where("author_id", $request->get("author_id"));
}
if ($request->has("search")) {
$query->where("title", "like", "%" . $request->get("search") . "%");
}
$books = $query->paginate(20);
return response()->json(BookResource::collection($books));
}
public function store(StoreBookRequest $request): JsonResponse {
$book = Book::create($request->validated());
return response()->json(new BookResource($book->load("author")), 201);
}
public function show(Book $book): JsonResponse {
return response()->json(new BookResource($book->load("author")));
}
public function update(StoreBookRequest $request, Book $book): JsonResponse {
$book->update($request->validated());
return response()->json(new BookResource($book->load("author")));
}
public function destroy(Book $book): JsonResponse {
$book->delete();
return response()->json(["message" => "Book deleted successfully"]);
}
}
6. Маршруты
<?php
use App\Http\Controllers\Api\AuthController;
use App\Http\Controllers\Api\AuthorController;
use App\Http\Controllers\Api\BookController;
use Illuminate\Support\Facades\Route;
// Публичные маршруты
Route::post("/register", [AuthController::class, "register"]);
Route::post("/login", [AuthController::class, "login"]);
// Защищённые маршруты
Route::middleware("auth:sanctum")->group(function () {
Route::post("/logout", [AuthController::class, "logout"]);
Route::apiResource("authors", AuthorController::class);
Route::get("/authors/{author}/books", [AuthorController::class, "books"]);
Route::apiResource("books", BookController::class);
});
7. Тесты
<?php
namespace Tests\Feature;
use App\Models\Author;
use App\Models\Book;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AuthorBookApiTest extends TestCase {
use RefreshDatabase;
protected $user;
protected $token;
protected function setUp(): void {
parent::setUp();
$this->user = User::factory()->create();
$this->token = $this->user->createToken("test-token")->plainTextToken;
}
public function test_register_user(): void {
$response = $this->postJson("/api/register", [
"name" => "John Doe",
"email" => "john@example.com",
"password" => "password123",
"password_confirmation" => "password123",
]);
$response->assertStatus(201);
$response->assertJsonStructure(["user", "token"]);
$this->assertDatabaseHas("users", ["email" => "john@example.com"]);
}
public function test_login_user(): void {
$response = $this->postJson("/api/login", [
"email" => $this->user->email,
"password" => "password", // Стандартный пароль из factory
]);
$response->assertStatus(200);
$response->assertJsonStructure(["user", "token"]);
}
public function test_create_author(): void {
$response = $this->withHeader("Authorization", "Bearer $this->token")
->postJson("/api/authors", [
"name" => "Leo Tolstoy",
"birth_date" => "1828-09-09",
"biography" => "Russian writer",
]);
$response->assertStatus(201);
$this->assertDatabaseHas("authors", ["name" => "Leo Tolstoy"]);
}
public function test_get_authors(): void {
Author::factory()->count(5)->create();
$response = $this->withHeader("Authorization", "Bearer $this->token")->getJson("/api/authors");
$response->assertStatus(200);
$response->assertJsonCount(5, "data");
}
public function test_create_book(): void {
$author = Author::factory()->create();
$response = $this->withHeader("Authorization", "Bearer $this->token")
->postJson("/api/books", [
"author_id" => $author->id,
"title" => "War and Peace",
"publication_year" => 1869,
"isbn" => "978-0-199-23256-4",
]);
$response->assertStatus(201);
$this->assertDatabaseHas("books", ["title" => "War and Peace"]);
}
public function test_get_author_books(): void {
$author = Author::factory()->create();
Book::factory()->count(3)->create(["author_id" => $author->id]);
$response = $this->withHeader("Authorization", "Bearer $this->token")
->getJson("/api/authors/$author->id/books");
$response->assertStatus(200);
$response->assertJsonCount(3, "data");
}
public function test_update_book(): void {
$book = Book::factory()->create();
$response = $this->withHeader("Authorization", "Bearer $this->token")
->putJson("/api/books/$book->id", [
"author_id" => $book->author_id,
"title" => "Updated Title",
]);
$response->assertStatus(200);
$this->assertDatabaseHas("books", ["title" => "Updated Title"]);
}
public function test_delete_author(): void {
$author = Author::factory()->create();
$response = $this->withHeader("Authorization", "Bearer $this->token")
->deleteJson("/api/authors/$author->id");
$response->assertStatus(200);
$this->assertDatabaseMissing("authors", ["id" => $author->id]);
}
public function test_unauthorized_request(): void {
$response = $this->postJson("/api/authors", ["name" => "Test"]);
$response->assertStatus(401);
}
}
8. Factories для тестирования
<?php
namespace Database\Factories;
use App\Models\Author;
use Illuminate\Database\Eloquent\Factories\Factory;
class AuthorFactory extends Factory {
protected $model = Author::class;
public function definition(): array {
return [
"name" => $this->faker->name(),
"birth_date" => $this->faker->date(),
"biography" => $this->faker->text(),
];
}
}
<?php
namespace Database\Factories;
use App\Models\Book;
use Illuminate\Database\Eloquent\Factories\Factory;
class BookFactory extends Factory {
protected $model = Book::class;
public function definition(): array {
return [
"author_id" => function () { return Author::factory(); },
"title" => $this->faker->sentence(),
"publication_year" => $this->faker->year(),
"isbn" => $this->faker->isbn13(),
"description" => $this->faker->text(),
];
}
}
Это полное решение REST API с авторизацией через Sanctum, управлением авторов и книг, Resource классами и полным покрытием тестами.