DEV Community

Cover image for Documenting Laravel APIs with Scramble
Sumeet Shroff
Sumeet Shroff

Posted on

Documenting Laravel APIs with Scramble

Documenting Laravel APIs with Scramble

If you have ever shipped a Laravel API and watched a frontend developer open Postman to reverse-engineer your endpoints, you know the pain of missing documentation. The traditional solution—Swagger annotations—requires you to decorate every controller method with dozens of lines of PHPDoc that drift out of sync the moment someone renames a parameter. Scramble takes a different approach: it reads your existing code and generates an OpenAPI 3.1 spec automatically, with zero annotations required.

This article walks through installing Scramble on a Laravel 11 or 12 project, what it infers automatically, how to fill in the gaps, and the tradeoffs you should know before adopting it in production.

Prerequisites: Laravel 11 or 12, PHP 8.2+, Composer. If you have not scaffolded your API routes yet, run php artisan install:api first—Laravel 11/12 no longer ships routes/api.php by default. See Building Production-Ready APIs with Laravel for the full setup walkthrough.


Why Scramble Instead of Swagger Annotations

The darkaonline/l5-swagger package (and its predecessor swagger-php) requires PHPDoc blocks like this on every endpoint:

/**
 * @OA\Get(
 *     path="/api/v1/posts",
 *     summary="List posts",
 *     tags={"Posts"},
 *     @OA\Parameter(name="page", in="query", ..."),
 *     @OA\Response(response=200, description="Success", ...)
 * )
 */
public function index(): AnonymousResourceCollection
{
    // ...
}
Enter fullscreen mode Exit fullscreen mode

That annotation block is longer than many controller methods. It also does not tell you when the annotation is wrong—if you change the response shape but forget to update @OA\Response, nobody finds out until a consumer hits a 500.

Scramble instead parses your controller return types, FormRequest rules, Eloquent API Resources, and route definitions. If your code is typed correctly, the documentation is correct by construction.


Installation

composer require dedoc/scramble
Enter fullscreen mode Exit fullscreen mode

Scramble auto-discovers itself via Laravel's package discovery. No service provider registration is needed. After installation, two routes are available:

  • /docs/api — Interactive Stoplight Elements UI
  • /docs/api.json — Raw OpenAPI 3.1 JSON spec

Both routes are restricted to the local environment by default. To expose them in staging or production, publish the config and adjust the middleware array:

php artisan vendor:publish --provider="Dedoc\Scramble\ScrambleServiceProvider"
Enter fullscreen mode Exit fullscreen mode

This creates config/scramble.php:

return [
    'api_path' => 'api',
    'api_domain' => null,
    'info' => [
        'version' => env('API_VERSION', '1.0.0'),
        'description' => '',
    ],
    'middleware' => [
        'web',
        RestrictedDocsAccess::class, // ships with Scramble
    ],
    'extensions' => [],
];
Enter fullscreen mode Exit fullscreen mode

To restrict docs to authenticated users in production, replace RestrictedDocsAccess with your own middleware or gate check.


What Scramble Infers Automatically

Route Parameters

Given a route like Route::get('/posts/{post}', [PostController::class, 'show']), Scramble reads the route binding and produces the correct {post} path parameter in the spec. If you use model binding (public function show(Post $post)), it infers the parameter type from the model's primary key type.

Request Body from FormRequest

Scramble reads rules() from a FormRequest and maps Laravel validation rules to JSON Schema types:

class StorePostRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'title'    => ['required', 'string', 'max:255'],
            'content'  => ['required', 'string'],
            'status'   => ['required', 'in:draft,published'],
            'tags'     => ['array'],
            'tags.*'   => ['string', 'max:50'],
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

Scramble converts in:draft,published to an OpenAPI enum, infers tags as an array of strings, and marks title, content, and status as required. You get accurate request body documentation without writing a single annotation.

Response Shape from API Resources

When a controller method returns a typed Resource or ResourceCollection, Scramble resolves the return type and inspects the toArray() method:

// app/Http/Resources/PostResource.php
class PostResource extends JsonResource
{
    public function toArray($request): array
    {
        return [
            'id'             => $this->id,
            'title'          => $this->title,
            'status'         => $this->status,
            'author'         => new UserResource($this->whenLoaded('author')),
            'comments_count' => $this->whenCounted('comments'),
            'created_at'     => $this->created_at->toISOString(),
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

Scramble picks up the field names and their types. It handles whenLoaded() by marking the nested resource as nullable/optional, which is accurate—the field only appears when the relationship is eager-loaded.

HTTP Status Codes

Scramble infers the success status code from what the controller returns. A response()->json([], 201) produces a 201 in the spec. Validation failures via FormRequest automatically produce a 422 response schema showing the standard Laravel error envelope.


Filling the Gaps with Attributes

Scramble cannot infer everything from code structure alone. For cases where inference falls short, it provides PHP 8 attributes instead of PHPDoc annotations.

Describe an Endpoint

use Dedoc\Scramble\Attributes\OperationId;
use Dedoc\Scramble\Attributes\Summary;
use Dedoc\Scramble\Attributes\Description;

#[Summary('Create a new post')]
#[Description('Stores a draft or published post. Requires the `posts.create` permission.')]
public function store(StorePostRequest $request): PostResource
{
    $post = Post::create($request->validated());
    return new PostResource($post);
}
Enter fullscreen mode Exit fullscreen mode

Document Query Parameters

Query parameters that are not in a FormRequest (filters, sort options, cursor values) need explicit documentation:

use Dedoc\Scramble\Attributes\QueryParameter;

#[QueryParameter('sort', description: 'Sort field. Allowed: created_at, title.', type: 'string', example: 'created_at')]
#[QueryParameter('direction', description: 'Sort direction.', enum: ['asc', 'desc'], default: 'desc')]
public function index(Request $request): AnonymousResourceCollection
{
    $posts = Post::with(['author'])
        ->withCount('comments')
        ->orderBy($request->input('sort', 'created_at'), $request->input('direction', 'desc'))
        ->paginate(20);

    return PostResource::collection($posts);
}
Enter fullscreen mode Exit fullscreen mode

Exclude Internal Endpoints

Not every route belongs in public documentation. Tag internal or admin-only endpoints to exclude them:

use Dedoc\Scramble\Attributes\ExcludeFromDocs;

#[ExcludeFromDocs]
public function internalMetrics(): JsonResponse
{
    // ...
}
Enter fullscreen mode Exit fullscreen mode

Versioned APIs

If you are running versioned route groups (the recommended approach for production APIs), configure Scramble to point at a specific API prefix:

// config/scramble.php
'api_path' => 'api/v1',
Enter fullscreen mode Exit fullscreen mode

For multiple versions with separate documentation UIs, register additional Scramble instances in a service provider:

// app/Providers/AppServiceProvider.php
use Dedoc\Scramble\Scramble;
use Dedoc\Scramble\Support\Generator\OpenApi;
use Dedoc\Scramble\Support\Generator\SecurityScheme;

public function boot(): void
{
    Scramble::registerApi('v2', [
        'api_path' => 'api/v2',
        'info' => ['version' => '2.0.0'],
    ]);
}
Enter fullscreen mode Exit fullscreen mode

This produces separate docs at /docs/v2 without conflating the two versions.


Authentication in the Spec

Scramble does not automatically detect that your routes are behind auth:sanctum. Register the security scheme manually:

// app/Providers/AppServiceProvider.php
use Dedoc\Scramble\Scramble;
use Dedoc\Scramble\Support\Generator\OpenApi;
use Dedoc\Scramble\Support\Generator\SecurityScheme;

public function boot(): void
{
    Scramble::configure()
        ->withDocumentTransformers(function (OpenApi $openApi) {
            $openApi->secure(
                SecurityScheme::http('bearer')
            );
        });
}
Enter fullscreen mode Exit fullscreen mode

This adds a BearerAuth security requirement globally. If some routes are public, apply ->withoutSecurity() per-route using the attribute approach.


Exporting the Spec for CI and Postman

Generate a static JSON spec on demand:

php artisan scramble:export
Enter fullscreen mode Exit fullscreen mode

This writes storage/api-docs/api.json by default. You can commit this file and fail the CI build if it changes unexpectedly, which forces developers to update documentation alongside code:

# .github/workflows/api-docs.yml (pseudocode)
- run: php artisan scramble:export
- run: git diff --exit-code storage/api-docs/api.json
Enter fullscreen mode Exit fullscreen mode

Import storage/api-docs/api.json directly into Postman using File > Import to generate a full collection with examples.


Limitations and Tradeoffs

What Scramble does not infer well:

  • Dynamic response shapes built with conditional logic inside toArray() beyond whenLoaded() and whenCounted(). If you use $this->when(someCondition(), ...) with complex expressions, the inferred schema may be incomplete.
  • Endpoints that return raw response()->json([...]) with an inline array instead of a typed Resource. Scramble cannot inspect an anonymous array literal for types.
  • Polymorphic relationships inside Resources. Scramble cannot resolve which of several model types might appear inside a morphTo() relationship without a hint.
  • Custom exception handlers that return non-standard JSON envelopes. If you override Handler::render(), Scramble may not know about your custom error shape.

Annotation fatigue is not zero. Scramble eliminates most annotations but not all. Query parameters, custom response descriptions, and security declarations still require attributes or document transformers.

The docs UI is not customisable without overriding the view. If your team requires Swagger UI instead of Stoplight Elements, you will need to point a separate Swagger UI instance at the generated JSON endpoint.

Scramble vs. l5-swagger: Scramble wins on maintenance cost (no annotation drift), accuracy (spec is derived from running code), and setup speed. l5-swagger wins when you need fine-grained control over every schema detail or are working with a legacy codebase that lacks return-type declarations and FormRequest classes.


Verifying Your Documentation

After setup, open /docs/api in your browser and cross-check:

  1. Every route in routes/api.php appears in the sidebar.
  2. FormRequest fields match what the endpoint actually accepts.
  3. Resource fields match the database columns and casts.
  4. Status codes are correct (201 for creation, 204 for deletion, 422 for validation).
  5. Bearer auth appears on protected routes.

For automated verification, use Spectral to lint the exported spec against the OpenAPI 3.1 ruleset:

npx @stoplight/spectral-cli lint storage/api-docs/api.json --ruleset @stoplight/spectral-owasp-ruleset
Enter fullscreen mode Exit fullscreen mode

The OWASP ruleset flags common API security issues (missing authentication declarations, overly permissive schemas) directly from the spec file.


Common Mistakes

  • Not typing controller return values. Scramble relies on PHP return type declarations. public function index() without a return type yields an empty response schema. Add AnonymousResourceCollection or PostResource return types everywhere.
  • Using response()->json($data) instead of Resources. Once you bypass the Resource layer, Scramble cannot inspect the shape. Reserve raw JSON responses for simple cases and document them with attributes.
  • Exposing the docs route in production without authentication. The default RestrictedDocsAccess middleware only allows local access. Wrap the docs routes with auth:sanctum or an IP allowlist before staging deployment.
  • Forgetting to re-export after schema changes. If your CI pipeline imports a stale spec into Postman or a mock server, consumers test against outdated contracts. Automate php artisan scramble:export in your deployment pipeline.
  • Running Scramble on routes that include Livewire or Inertia endpoints. Set api_path precisely to your API prefix so Scramble does not attempt to parse server-rendered page routes.

Scramble is the lowest-friction path to accurate OpenAPI documentation for Laravel APIs. It will not eliminate every annotation, but it eliminates the most tedious ones—the ones that duplicate information already present in your types, FormRequests, and Resource classes.

If you need Laravel development in Mumbai, Mumbai Web Designer builds production-grade Laravel applications.

Top comments (0)