← Назад к вопросам

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 классами и полным покрытием тестами.

REST API для авторов и книг | PrepBro