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
{
// ...
}
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
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"
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' => [],
];
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'],
];
}
}
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(),
];
}
}
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);
}
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);
}
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
{
// ...
}
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',
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'],
]);
}
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')
);
});
}
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
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
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()beyondwhenLoaded()andwhenCounted(). 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:
- Every route in
routes/api.phpappears in the sidebar. - FormRequest fields match what the endpoint actually accepts.
- Resource fields match the database columns and casts.
- Status codes are correct (201 for creation, 204 for deletion, 422 for validation).
- 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
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. AddAnonymousResourceCollectionorPostResourcereturn 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
RestrictedDocsAccessmiddleware only allows local access. Wrap the docs routes withauth:sanctumor 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:exportin your deployment pipeline. -
Running Scramble on routes that include Livewire or Inertia endpoints. Set
api_pathprecisely 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)