Skip to content

Your first export

With Docuccino installed, you can document an existing API without writing a single annotation. This page runs the export, shows exactly what came out, and points you at the viewer and CI.

Terminal window
php artisan docuccino:export
Wrote /home/you/invoices-api/docs/openapi.json (openapi-3.2).

With no arguments this builds every configured document and writes each to its own export.path. The published default writes one document to docs/openapi.json as OpenAPI 3.2, creating the directory if it doesn’t exist.

Docuccino walks each route that matches your document’s filters and assembles an operation from your code. Here’s a list endpoint you’d write anyway, and the operation it produces:

app/Http/Controllers/InvoiceController.php
final class InvoiceController
{
/**
* List invoices, filterable and sortable via the query string.
*/
public function index(): AnonymousResourceCollection
{
$invoices = QueryBuilder::for(Invoice::class)
->allowedFilters(['number', AllowedFilter::exact('status')])
->allowedSorts(['issued_at', 'total'])
->defaultSort('-issued_at')
->paginate(20);
return InvoiceResource::collection($invoices);
}
}

Nothing in that controller is written for the docs. Every line of the output is derived:

Output Where it came from
operationId The route’s name, invoices.index.
summary The action’s docblock summary.
tags The controller name, InvoiceController → Invoice.
filter[number], filter[status] The allowedFilters() allow-list, traced through the builder. status is an array of the InvoiceStatus enum because the model casts that column to it — and because Query Builder matches a comma-separated list as whereIn.
sort An enum of the allowedSorts() names, ascending and descending both, with defaultSort() as the schema default.
page The paginate(20) call. The 20 is fixed at that call site, so no page-size key is documented beside it — nothing reads one.
200 InvoiceResource::collection() over a paginated result: an array of the shared InvoiceResource schema inside Laravel’s length-aware { data, links, meta } envelope (trimmed above).
400 Query Builder’s strict mode, which rejects an unknown filter or sort with InvalidQuery.

Add a StoreInvoiceRequest to a store() action and its rules() become the request body. Throw InvoiceNotFoundException and a 404 appears with the shape your handler really returns. That’s the whole loop — write ordinary Laravel, get documentation.

How it works is the mechanism behind that table: what Docuccino reads at each stage of the pipeline, and which source wins when two of them describe the same field.

The built-in viewer serves an interactive Scalar reference straight from your app. With the default viewer.route of /docs/api, three routes are registered per document:

Route Serves
GET /docs/api The interactive API reference page.
GET /docs/api.json The generated OpenAPI document.
GET /docs/api/assets/scalar.js The viewer script, served from your app — no external CDN.

The viewer answers in your local environment and nowhere else. To open it up elsewhere, name a gate ability and define it:

  1. Set the ability in config/docuccino.php, which is where the viewer is configured:

    documents.default.viewer.gate
    'gate' => 'viewApiDocs',
  2. Define it in a service provider:

    Gate::define('viewApiDocs', fn ($user) => $user?->isAdmin() ?? false);

Once gate is set, it is the only check — the local-environment shortcut no longer applies, so the same rule holds in every environment.

docuccino:export emits OpenAPI 3.2 by default. --format picks another; --yaml is a serialization rather than a format, so it applies to whichever of them you asked for:

Terminal window
php artisan docuccino:export --out=public/openapi.json

The full document is what Docuccino builds before emitting: the OpenAPI it would export, plus the Docuccino extension carrying a stable identity for every operation and schema and the provenance of each detail. It’s what docuccino:diff compares, so commit it when you want the most precise diffs.

The Arazzo file describes the workflows a document declares — the sequences of calls a consumer follows to get something done. It’s written only when the document carries at least one, because Arazzo has no empty form.

If you ship more than one of these, configure them as targets in docuccino.yaml rather than running the command once per format — analysis is the expensive half, and targets share one:

documents:
default:
export:
targets:
- { format: 'openapi-3.2', path: 'docs/openapi.json' }
- { format: 'openapi-3.1', path: 'docs/openapi-3.1.yaml' }
- { format: 'postman', path: 'docs/collection.json' }
Terminal window
php artisan docuccino:export
# Wrote /home/you/invoices-api/docs/openapi.json (openapi-3.2).
# Wrote /home/you/invoices-api/docs/openapi-3.1.yaml (openapi-3.1).
# Wrote /home/you/invoices-api/docs/collection.json (postman).

The path’s extension picks the serialization — no --yaml needed. See export for the full rules.

Reach for --format=openapi-3.0 when something downstream is pinned to 3.0 — AWS API Gateway, an older code generator, a validator that predates 3.1. It writes openapi: 3.0.4.

3.0 is a smaller spec than the one Docuccino builds against, so things change shape on the way down. Each one prints a downlevel.* diagnostic naming the JSON pointer it happened at, so you can see exactly what a 3.0 consumer will and won’t get.

A 3.0 export is produced through the 3.1 one, so it carries everything the 3.1 target drops as well as its own losses. The first group below is what 3.1 already gives up; the rest is 3.0’s:

In 3.2 In the 3.1 and 3.0 exports
The query HTTP method Dropped — the operation is not in the emitted document
additionalOperations Dropped — model custom methods with a standard method
A tag’s summary Dropped — tags fall back to their name for display
A tag’s parent Dropped; the hierarchy survives as x-tagGroups, which a generated document carries
A tag’s kind Dropped — consumers treat every tag the same
Any other member 3.2 added — a media type’s description, itemSchema, prefixEncoding and itemEncoding, an encoding’s nested encoding, an example’s dataValue and serializedValue, a response summary, a server name, a security scheme’s deprecated and oauth2MetadataUrl, an OAuth deviceAuthorization flow, components.mediaTypes Dropped where it stood, with the pointer in the diagnostic. A shared media type is inlined where a $ref named it, so no reference dangles
An in: querystring parameter Dropped — 3.1 has no way to say “the raw query string as one value”, and a query parameter is a different contract. A shared one takes every $ref that named it with it
A cookie parameter’s style: cookie Dropped, and nothing written in its place — form is the only cookie style 3.1 spells and its default there, and both styles default explode to true. What 3.1 cannot say is the rest of RFC 6265: form percent-encodes where cookie escapes nothing, and joins an exploded array or object on & rather than ;
In 3.1 / 3.2 In the 3.0 export
type: [string, null] type: string with nullable: true
anyOf: [{$ref}, {type: null}] allOf: [{$ref}] with nullable: true
type: [string, integer] An anyOf of single-type branches
const: "draft" enum: ["draft"]
A subschema of true or false {} or {"not": {}} — the same constraint, in the only spelling 3.0 has for it
Schema examples: [a, b] example: a
exclusiveMinimum: 0 minimum: 0 with exclusiveMinimum: true
contentEncoding: base64 format: byte
A description beside a $ref Hoisted, with the $ref moved into an allOf
$anchor, $defs, $id, $schema, additionalItems, contains, contentMediaType, contentSchema, definitions, dependentRequired, dependentSchemas, else, if, maxContains, minContains, patternProperties, prefixItems, propertyNames, then, unevaluatedItems, unevaluatedProperties Dropped — 3.0 cannot express them
webhooks, components.pathItems, info.summary Dropped — 3.0 has no such member
A path $ref-ing a shared path item Inlined where it stands, since the shared one is gone
A path whose $ref chain reaches no shared path item Dropped — an undefined name or a cycle leaves nothing to inline, so a 3.0 consumer loses the endpoint
An SPDX info.license.identifier info.license.url, pointing at the SPDX page
A mutualTLS security scheme Dropped, along with every requirement that named it
An operation with no responses A placeholder default response — 3.0 requires every operation to declare at least one

Everything the two tables don’t name is identical to the 3.2 output, bar one thing no diagnostic can tell you about: a schema’s $comment is dropped without a note, because it addresses whoever wrote the schema and says nothing a consumer could act on. Keep the 3.2 artifact around as the full-fidelity copy — it is the only one that loses nothing — and treat the 3.0 one as what you hand to the pinned toolchain. The diagnostics reference has a row per downlevel.* code, including the ones that only fire on documents using the construct.

See the commands reference for every flag.

Docuccino reports diagnostics — a controller it couldn’t read, a query it couldn’t resolve, a property name that looks like a secret — grouped by route, in a deterministic order. Turn them into a failing build:

Terminal window
php artisan docuccino:export --fail-on=warning

The value is a floor — anything that severity or louder fails the run. --fail-on=none never fails on severity (the default), --fail-on=error fails only on errors, --fail-on=warning adds warnings, --fail-on=info adds the places Docuccino documented less than your code describes, and --fail-on=hint catches everything it reported. Whichever you pick, a route whose analysis fails still leaves a stub operation and a diagnostic behind rather than aborting the run, so one bad controller never costs you the rest of your docs. Set on_route_error to 'omit' if you’d rather such a route vanished from the document than appeared as a stub.

If your own export came out thinner than the one above — parameters but no response bodies, an engine.not-installed warning, or nothing at all — Troubleshooting starts from the symptom and works back to the cause.

You’ve seen the base export. Documenting your API is the reference for everything Docuccino reads — requests, responses, schemas, errors, authentication, rate limiting — and where to reach for an attribute when your code can’t say it all.