g4t/swagger
g4t/swagger is a Laravel package that generates Swagger/OpenAPI documentation from your routes and annotations. It automates API spec creation, keeps docs aligned with your code, and simplifies serving interactive documentation.
Install and publish
composer require g4t/swagger
php artisan vendor:publish --tag=swagger-config # or --provider="G4T\Swagger\SwaggerServiceProvider"
php artisan vendor:publish --tag=swagger-assets # swagger-ui assets, if the package ships them
If the tag names differ, run php artisan list | grep -i swagger and inspect vendor/g4t/swagger/src/ — this package is small enough to read end-to-end in ten minutes, and you should, because the surface API is not extensively documented.
Where to look first
config/swagger.php (or g4t-swagger.php) — base path, output file, scan paths, UI route.vendor/g4t/swagger/src/Console/ — the generation command and its flags.vendor/g4t/swagger/src/routes/ — the docs route the package registers, and its middleware.swagger.json / api-docs.json — this is your source of truth when debugging, not the UI.First use case
Document a single CRUD resource before touching anything else:
# 1. Point the scanner at one controller only
# config: 'paths' => [app_path('Http/Controllers/Api/UserController.php')]
# 2. Generate
php artisan swagger:generate
# 3. Serve
# visit the route the package registers (commonly /api/documentation)
Then open the generated JSON file directly and confirm the paths, parameters, and components.schemas blocks match what you expect. If the UI renders but the JSON is thin, you have an annotation/scanning problem, not a UI problem — always debug against the JSON.
Environment gating
Register/serve the docs route only outside production (or behind auth). The package usually reads a config flag; wrap it yourself if it doesn't:
// AppServiceProvider::boot()
if (! app()->isProduction()) {
// route is auto-registered by the package; otherwise include its route file here
}
1. Annotate at the controller, let the scanner fill the rest
The package's value proposition is deriving schemas from your code. In practice that means: annotate the request body and responses explicitly, and let route registration supply the path, method, and path parameters. Anything relying on inference from bare array return types will come out as an empty object.
/**
* @OA\Post(
* path="/api/users",
* tags={"Users"},
* @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/UserRequest")),
* @OA\Response(response=201, description="Created", @OA\JsonContent(ref="#/components/schemas/UserResource"))
* )
*/
public function store(StoreUserRequest $request): UserResource
2. Drive schemas from FormRequests and API Resources
The highest-leverage habit: one components.schemas entry per FormRequest (input) and per JsonResource (output), reused via $ref. This keeps validation rules and docs from drifting, because both live next to each other. Where the package can infer rules() it will; where it can't (Rule::unique, closures, Rule::in), hand-write the property.
3. Split generation by API surface
For multi-version or multi-module APIs, run the generator once per directory into separate files rather than one monster document:
// config: 'paths' => [app_path('Http/Controllers/Api/V1')], 'output' => 'public/docs/v1.json'
// then a second entry for V2 with its own output
Then serve each with its own UI mount or a small swagger-ui config selecting the file. This avoids components.schemas name collisions between versions — the most common breakage in versioned APIs.
4. Generate in CI, serve statically
Treat the spec as a build artifact:
# deploy step
- php artisan swagger:generate
- test -s public/docs/api.json # fail the build if generation broke
Committing the JSON to the repo gives you diffable API contract changes in PRs, which is the single best reason to use a package like this. Dynamic generation on every request is fine locally but wasteful in production.
5. Protect the docs route
If the UI route is auto-registered, wrap it with the app's auth middleware (or an IP allowlist) in config / the service provider. Publicly exposed Swagger UIs are a routine finding in API pentests, and this package does not decide that for you.
6. Use it as a starting point, not a contract
The realistic workflow: generate once, then hand-edit / annotate the 20% of endpoints the scanner gets wrong. The scanner gets you 70% cheaply; treat the remaining 30% as annotation work you would have done anyway with zircote/swagger-php.
Auto-scanning blind spots
__invoke returns, and Route::resource expansions often produce skeletal or missing paths. Verify route count in the JSON against php artisan route:list --path=api.app/Http/Controllers (modular/Domain layouts) are silently skipped unless you add the path in config.JsonResponse, Response, or mixed yield empty schemas — annotate them explicitly.Route caching
php artisan route:cache will freeze the package's docs route registration and any config-driven route logic. Clear and re-cache after changing swagger config; in CI always route:clear before swagger:generate.
Stale output The most common "bug report" is simply old JSON. If the UI shows an endpoint you deleted, you didn't regenerate. Add generation to the deploy pipeline rather than relying on developers to remember.
Type inference failures
$request->all() payloads → empty properties.?string) sometimes emit type: string without nullable: true; check and correct.string — add the enum list by hand.Config quirks
app_path() are safest.vendor/ before concluding the config is ignored.servers/host + basePath; a mismatch here makes working endpoints look broken in the browser and sends people debugging the wrong layer.Debugging checklist
cat public/docs/*.json | jq '.paths | keys' — are your routes present?jq '.components.schemas | keys' — did schemas generate?-v (or equivalent) to surface scanner parse errors — a single malformed docblock can abort scanning for that file silently.npx @redocly/cli lint) before blaming the package for UI rendering issues.Extension points
zircote/swagger-php in most builds; if you need behavior it lacks, subclass or re-invoke the underlying scanner directly rather than forking the package.components.schemas via a dedicated annotations file included in the scan paths — this is the cleanest way to document shared DTOs and error envelopes (ErrorResponse, pagination wrappers) that no controller owns.Maintenance risk — read this before committing
124 stars, zero dependents, and a single-maintainer bus factor mean you should: pin an exact version in composer.json, commit composer.lock, and be prepared to swap to darkaonline/l5-swagger + hand-written annotations if the package goes quiet. The annotations you write are portable; the generator wrapper is not. Keeping the annotated portion of your spec package-agnostic is cheap insurance and the main architectural decision to make on day one.
How can I help you explore Laravel packages today?