Profile    Mohammed Shiroz Status   Loading  
Logo
Share This
Back to blog
Filter by:
Tags
//Article title

Building a REST API in Laravel 12 With OpenAPI Docs That Stay Accurate

About Post

The mobile developer messages you: "What does the employees endpoint return when the department is missing? And is it joined_at or join_date?" You check the wiki page. It says start_date. Nobody has used that name for a year.

An API without documentation gets used wrongly. An API with outdated documentation is worse, because people trust it. The fix is docs that are generated from, or live right next to, the code.

Let's build a small, clean REST API in Laravel 12, step by step, and then add OpenAPI (Swagger) docs in two different ways. The example is an employees resource, the kind you'd find in any HR system. My own personal Laravel 12 HR project documents its API with Swagger/OpenAPI, and it's one of those decisions that pays for itself the first time someone else has to call your endpoints.

Step 1: set up the API layer

A fresh Laravel 12 app doesn't include API routes by default. One command adds routes/api.php and installs Sanctum for token authentication:

php artisan install:api

Then the routes. apiResource registers the five standard endpoints (index, store, show, update, destroy) without the HTML-only create and edit:

// routes/api.php
Route::middleware('auth:sanctum')->group(function () {
    Route::apiResource('employees', EmployeeController::class);
});

Run php artisan route:list --path=api and you'll see them, all prefixed with /api.

Step 2: validation in a Form Request

Keep validation out of the controller. A Form Request validates before your controller method even runs, and returns a 422 with field errors as JSON when the client sends Accept: application/json:

class StoreEmployeeRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'name'          => ['required', 'string', 'max:255'],
            'email'         => ['required', 'email', 'unique:employees,email'],
            'department_id' => ['nullable', 'exists:departments,id'],
            'joined_at'     => ['required', 'date'],
        ];
    }
}

Step 3: shape the output with an API Resource

Never return a model directly. The day someone adds a sensitive column to the table, it's in your API. A Resource is an explicit contract of what leaves your app:

class EmployeeResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id'         => $this->id,
            'name'       => $this->name,
            'email'      => $this->email,
            'department' => new DepartmentResource($this->whenLoaded('department')),
            'joined_at'  => $this->joined_at?->toDateString(),
        ];
    }
}

whenLoaded only includes the department if you eager-loaded it, which quietly protects you from N+1 queries.

Step 4: a thin controller

class EmployeeController extends Controller
{
    public function index(Request $request)
    {
        $perPage = min($request->integer('per_page', 15), 100);

        return EmployeeResource::collection(
            Employee::with('department')->latest()->paginate($perPage)
        );
    }

    public function store(StoreEmployeeRequest $request)
    {
        return new EmployeeResource(Employee::create($request->validated()));
    }

    public function show(Employee $employee)
    {
        return new EmployeeResource($employee->load('department'));
    }
}

A few things you get for free here. Paginated collections include links and meta automatically. A freshly created model returned through a Resource gets a 201 status without you setting it. And route model binding returns a 404 for a missing employee. Cap per_page like above, or someone will eventually ask for a million rows.

Step 5: make every error JSON

By default, Laravel renders errors as JSON when the request asks for JSON. Clients don't always send the right header, so in bootstrap/app.php make it unconditional for API routes:

->withExceptions(function (Exceptions $exceptions) {
    $exceptions->shouldRenderJsonWhen(
        fn (Request $request, Throwable $e) => $request->is('api/*') || $request->expectsJson()
    );
})

Step 6: OpenAPI docs, two ways

OpenAPI is a standard format for describing an API: its paths, parameters, request bodies and responses. "Swagger" is the older name, and Swagger UI is the interactive page most people picture, with a "Try it out" button on each endpoint. Two popular routes in Laravel:

Option A: describe it with attributes (swagger-php / L5-Swagger)

The darkaonline/l5-swagger package uses swagger-php, where you describe each endpoint with PHP attributes right above the method:

use OpenApi\Attributes as OA;

#[OA\Get(
    path: '/api/employees/{employee}',
    summary: 'Show one employee',
    security: [['sanctum' => []]],
    tags: ['Employees'],
    parameters: [new OA\Parameter(name: 'employee', in: 'path', required: true,
        schema: new OA\Schema(type: 'integer'))],
    responses: [
        new OA\Response(response: 200, description: 'The employee'),
        new OA\Response(response: 404, description: 'Employee not found'),
    ],
)]
public function show(Employee $employee) { /* ... */ }

Generate the spec with php artisan l5-swagger:generate and the UI is served at /api/documentation by default. You also need one OA\Info attribute (title and version) somewhere in your app, plus a security scheme definition for the bearer token.

Pros: full control, very explicit. Cons: it's verbose, and because the attributes are written by hand, they can still drift from the code if nobody updates them.

Option B: infer it from the code (Scramble)

dedoc/scramble takes the opposite approach: it reads your routes, Form Request rules and Resource classes and builds the OpenAPI document automatically, with no annotations. Install it and the docs appear at /docs/api (by default only in the local environment).

Pros: close to zero effort, and it changes when the code changes. Cons: it can only document what it can infer, so complex responses sometimes need a type hint or a small annotation to come out right.

My rule of thumb: prefer docs that are generated from the code that actually runs. If you write them by hand, regenerate the spec in CI and fail the build if it's invalid, so "the docs are broken" is caught in a pull request instead of by a frustrated client developer.

Small things that make an API pleasant

  • Consistent names. Pick snake_case or camelCase for JSON keys and never mix them.
  • Dates as ISO 8601 strings, always in the same timezone convention.
  • Version from day one (/api/v1), even if you hope you'll never need v2.
  • Feature tests per endpoint that assert the JSON structure with assertJsonStructure. They're your second, executable form of documentation.

Recap

Routes with apiResource, validation in Form Requests, output through Resources, JSON errors everywhere, and an OpenAPI spec that's generated rather than remembered. That's a REST API other people can actually use without messaging you.

Do you write your OpenAPI docs by hand, annotate the code, or generate them automatically? And what pushed you that way?

Comments (0)
Leave your review

Thanks for your valuable comments. Your comments has been updated and appreciate your getting in touch...

01. About Shiroz

Mohammed Shiroz

Hi, I'm Mohammed Shiroz, a software engineer and AI enthusiast from Sri Lanka who turns ideas into intelligent, real-world solutions. With over 9 years of hands-on experience, I currently lead real estate ERP development at Kate Group, a...

03.My Projects

04. Categories

Ready To order Your Project ?

Get in Touch
Close