knuckleswtf/scribe
Scribe generates human-friendly API documentation from your Laravel codebase. It extracts request parameters from FormRequests or validation rules, calls endpoints for sample responses, and outputs a single-page HTML doc with a Try It Out tester, plus Postman collections and OpenAPI specs.
Scribe is a dev-only dependency that turns your existing Laravel routes, FormRequests and API Resources into documentation. You rarely write docs by hand — you annotate, then generate.
composer require --dev knuckleswtf/scribe
php artisan scribe:install # publishes config/scribe.php and picks laravel/laravel mode
php artisan scribe:generate # writes public/docs/{index.html,collection.json,openapi.yaml}
Then open http://your-app.test/docs (or public/docs/index.html) — the default theme includes "Try It Out" against your configured base URL.
Look here first, in this order:
config/scribe.php — read it top to bottom once. The keys that matter day-to-day are type, routes.include/exclude, auth, example_languages, groups, strategies, and openapi.version.config/scribe.php → routes — by default Scribe documents everything in routes/api.php. Tighten this immediately so internal/admin routes don't leak into public docs:'routes' => [
'include' => [ 'api/*' ],
'exclude' => [ 'api/internal/*', 'api/health' ],
],
app/Http/Requests/* and app/Http/Resources/* — Scribe reads these automatically, so most endpoints need zero annotations.First use case. Take one controller action, add a group and let validation rules do the parameter docs:
use Knuckles\Scribe\Attributes\Group;
use Knuckles\Scribe\Attributes\UrlParam;
use Knuckles\Scribe\Attributes\ResponseFromApiResource;
#[Group('Posts', 'Create, read, update and delete posts')]
class PostController extends Controller
{
#[UrlParam('post', 'The post ID', example: 1)]
#[ResponseFromApiResource(PostResource::class, Post::class, collection: false)]
public function show(ShowPostRequest $request, Post $post) { /* ... */ }
}
Run php artisan scribe:generate and that endpoint now has a description derived from your docblocks, a request body derived from ShowPostRequest::rules(), a sample response derived from PostResource, and code samples in every language listed in example_languages.
Metadata-first, annotation-second. Scribe's extraction strategies are ordered: FormRequests and Validator::make() rules give you body/query params for free; ApiResource/Transformer classes give you response schemas. Only reach for #[BodyParam], #[QueryParam], #[HeaderParam], #[UrlParam] and #[Response] when the inference is wrong or the data doesn't live in code (webhooks, third-party payloads). Over-annotating is the most common way docs drift out of sync with reality.
Group by resource, control order in config. Use #[Group('Posts', 'description')] (or @group in the docblock for older codebases) consistently, then fix navigation order once:
'groups' => [
'order' => ['Authentication', 'Users', 'Posts', 'Webhooks'],
'default' => 'Endpoints',
],
Pick a response strategy per endpoint, not per project.
response_calls.enabled = true) — best fidelity, but hits your real app and DB.#[ResponseFromApiResource(PostResource::class, Post::class)] with a factory — deterministic, readable, no HTTP round trip.#[Response(['data' => ['id' => 1]])] — hand-written JSON for the awkward 10%.A pragmatic mix: live calls for read-only GETs, resource-derived samples for writes.
Wire outputs to their consumers. Generate all three formats and let each team use what they need — HTML for PMs/support, openapi.yaml for frontend codegen (orval, openapi-generator, typed clients), collection.json for QA in Postman. Disable what nobody consumes:
'postman' => ['enabled' => true, 'overrides' => ['info.version' => '1.0.0']],
'openapi' => ['enabled' => true, 'version' => '3.1.0'],
Run it in CI, not on someone's laptop. Regenerate on every deploy so docs can't rot. Because response calls need a working DB and app, give it an isolated environment:
- run: php artisan migrate --seed
- run: php artisan scribe:generate --force
- uses: actions/upload-artifact@v4
with: { name: api-docs, path: public/docs }
If your DB is unavailable in CI, generate with response calls off and rely on #[ResponseFromApiResource] — php artisan scribe:generate --help lists the current flag names, which have shifted between majors.
Multiple audiences, multiple configs. For a public API and an internal/partner API, keep two config files (e.g. scribe.php, scribe_internal.php) and generate each with --config=; the docs cover the exact flag for your version. This beats one sprawling doc with everything hidden behind #[hideFromAPIDocumentation].
Use the extension points instead of forking. strategies.* lets you drop in your own extractor classes, and recent 5.x releases expose an afterExtracting hook where you can enrich endpoint data (add a deprecated note, rename fields, inject a shared error envelope). base_url even supports Blade syntax, which is handy for multi-tenant doc builds. Custom response content types (application/problem+json) are supported — configure them in response_calls/#[Response] rather than letting Scribe assume JSON.
Auth and Try It Out. Set auth.enabled, auth.in, auth.name and auth.use_value: false so callers see a placeholder token instead of a real one. If your docs sit behind a session-authenticated app, enable try_it_out.use_csrf and point csrf_url at /sanctum/csrf-cookie or /csrf-token.
POST /posts during generation really creates a Post. Do not point scribe:generate at production. Always list your connections in database_connections_to_transact so each response call is rolled back — and prefer a throwaway SQLite/in-memory DB for docs builds.#[ResponseFromApiResource] with Model::factory()->create() can trigger observers, notifications, and search-index pushes. 5.8 added an opt-in to suppress model events during sample generation — turn it on in any app with heavy observers.auth for response calls or mark those endpoints with hand-written #[Response]. Also check storage/logs/laravel.log — Scribe swallows many extraction errors and moves on.#[UrlParam('post', 'The post ID', example: 1)] or bind on a UUID/slug, otherwise your frontend team will send IDs where you expect slugs.nullable: true; 3.1.0 uses JSON-Schema type: ["string","null"]. Codegen tools differ wildly in what they accept — pick early and pin it, because switching later changes every generated schema.BelongsTo loading (5.11), HTML special characters breaking the Try It Out JSON panel (5.7). If something looks wrong, check the changelog before debugging your annotations.public/docs and vendor assets. Any hand-edited HTML is lost. Publish and edit the Blade views under resources/views/vendor/scribe instead; do the same for intro_text (Markdown) and custom UI partials.#[Deprecated] is documentation-only. It adds a badge and OpenAPI deprecation flag; it does not affect routing or emit headers. Pair it with a real Sunset header if you actually want consumers to notice.openapi.yaml in CI is worse than none — always generate rather than commit-and-forget.example values, intro_text, seeded factory data, and base_url all end up in a static file that's often deployed publicly. Use fake hosts, fake tokens, and fake emails — never real customer records pulled from a seeded dev DB.How can I help you explore Laravel packages today?