Most Laravel teams reach a point where their API documentation is either out of date, written somewhere it shouldn’t be (Confluence, anyone?), or just doesn’t exist. Scribe is the package that quietly fixes this — it reads your routes, controllers, and docblocks, and turns them into a polished, browsable docs page. Less work for you, fresher docs for whoever consumes your API. 🎉
What Scribe actually does
You annotate controllers with familiar phpDoc tags — @group, @urlParam, @queryParam, @responseFile — and Scribe extracts everything into intermediate YAML files under .scribe/. Those YAMLs are your editable source of truth: you can hand-tweak descriptions, add example values, mark endpoints deprecated, and so on. Scribe then renders them into a Blade view (or static HTML, your choice) that ships with your app.
A typical controller looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | /** * @group Campaign Management * * APIs for creating and managing campaigns. */ class CampaignController extends Controller { /** * Retrieve All Campaign Data (Paginated). * * @queryParam page integer Current page number. Example: 1 * @queryParam per_page integer Items per page (max 100). Example: 10 * @queryParam filter[status] Filter by status. Enum: draft, scheduled, in_progress, ended. * @responseFile storage/responses/campaigns.index.json */ public function index(Request $request) { /* ... */ } } |
That’s it. The first paragraph of the docblock becomes the title; the rest is the description. @responseFile points to a JSON fixture so your example responses don’t depend on a live database during doc generation. 💡
How scribe:generate works
One command does the whole job:
1 | php artisan scribe:generate |
Behind the scenes, this is a two-phase pipeline:
- Extraction. Scribe walks your routes, parses each controller’s docblocks, and writes .scribe/endpoints.cache/*.yaml (its internal source of truth) and .scribe/endpoints/*.yaml (the user-overridable copies). If you’ve manually edited the non-cache files, Scribe respects your edits on the next run.
- Rendering. Scribe takes the YAML, applies its template, and writes the docs. With type: laravel, you get resources/views/scribe/index.blade.php served via a normal Laravel route — usually something like /api/v1/docs behind your auth middleware. With type: static, you get a self-contained HTML bundle in public/docs/.
The neat part: .scribe/ is meant to be checked into git. Every regenerate produces a diff you can review. If a teammate’s PR changes a route’s signature, that diff shows up in code review. Documentation drift becomes visible.
Upgrading Scribe without breaking your docs page
Recently I worked through an upgrade from Scribe 5.1 to 5.9 on a real project. The trigger was a Composer warning that nobody had been able to silence:
1 2 | Package spatie/data-transfer-object is abandoned, you should avoid using it. Use spatie/laravel-data instead. |
The first instinct was to migrate from spatie/data-transfer-object to spatie/laravel-data. But a quick grep across the codebase showed something interesting: nothing in the app actually used Spatie\DataTransferObject. Zero imports, zero extends. The package was a transitive dependency — pulled in by Scribe itself.
Composer prints abandonment notices for every package in the resolved tree, including transitives. So the right fix wasn’t to migrate our code; it was to upgrade Scribe to a version that had dropped the abandoned dependency. A bit of digging through GitHub tags revealed that Scribe 5.4.0 (released October 2025) was the first release to remove it.
The actual upgrade — and the safeguard
Once you know which version to target, the upgrade itself is one command:
1 | composer require --dev "knuckleswtf/scribe:^5.4" --with-all-dependencies |
The –dev flag matters because Scribe lives in require-dev; without it, Composer would happily move the package to require. –with-all-dependencies lets Composer bump anything Scribe depends on. The lockfile gets rewritten cleanly; no composer install needed afterward.
But the real question is: does the upgrade break anything? This is where the committed .scribe/ directory pays you back. The whole verification dance is:
1 2 3 4 5 6 7 8 9 10 11 | # Make sure your baseline is committed first git status .scribe/ # Upgrade Scribe composer require --dev "knuckleswtf/scribe:^5.4" --with-all-dependencies # Regenerate php artisan scribe:generate # The diff is your safeguard git diff .scribe/ |
In my case, the diff was almost entirely schema additions: every endpoint and parameter gained a deprecated: false field, and the Blade template’s JS asset bumped from theme-default-5.1.0.js to theme-default-5.9.0.js. No endpoints disappeared, no parameter descriptions got mangled, no response examples changed. A clean upgrade. ✅
The smoke test that catches what diffs miss
YAML diffs tell you about extracted data. They don’t tell you what an actual API consumer sees. After every Scribe upgrade, open the live docs URL in a browser:
- Does the page render at all?
- Is the base URL correct? (If you’ve configured a placeholder like https://mydomain.com and rewrite it at runtime to the tenant host, this is where you find out the rewrite still works.)
- Do the example curl commands look right?
- Does the auth section show the bearer scheme you configured?
This three-minute check catches everything the YAML diff can’t see — Blade template breakage, missing CSS assets, broken navigation. If the page loads and the example requests look sane, you’re done.
Worth knowing
A few things I’d tell my past self before starting:
- Commit .scribe/ to git. Treat it like a build artifact you want diffable. Without that baseline, you can’t tell whether a regeneration changed anything meaningful.
- Faker can introduce noise. Parameters without an explicit Example: value get random Faker output, which differs every run. If your diffs are noisy across regenerations even without a Scribe upgrade, that’s the cause. Pin examples in your docblocks for stable diffs.
- Packagist’s API can lie. When researching which Scribe version dropped a dependency, I found Packagist’s v2 endpoint serving stale require data for some tags. The authoritative source is the actual composer.json in the GitHub tag — https://raw.githubusercontent.com/knuckleswtf/scribe/<tag>/composer.json.
Scribe is one of those packages that quietly removes a class of recurring chores from your day. Stale docs, undocumented endpoints, the awkward Confluence page nobody updates — all gone, replaced by something that lives next to the code and gets regenerated as part of your normal workflow. And when it’s time to upgrade, the same .scribe/ directory that powers your docs becomes the thing that tells you whether the upgrade was safe. Boring. Useful. Exactly what you want from a tool. 🐘