api-skeletons/laravel-api-problem
Laravel API Problem provides RFC 7807-compliant problem details responses for JSON APIs, helping you return consistent, machine-readable errors with exception and validation support. It integrates with Laravel to standardize API error payloads and status codes.
composer require api-skeletons/laravel-api-problem. Laravel package auto-discovery registers the service provider; no manual providers entry needed on 9/10/11/12.php artisan vendor:publish --tag=api-problem → config/api-problem.php. Read this file first — it tells you which defaults (title/status/about:blank handling) the package will apply when you throw a bare exception.{"message": "..."} error and make it return an RFC 7807 application/problem+json body instead. Register the package middleware on your API group (Laravel 11/12: bootstrap/app.php ->withMiddleware(...); Laravel 10 and below: $middlewareGroups['api'] in app/Http/Kernel.php), then throw the package's ApiProblemException from a controller with status, title, detail, and instance.curl -i the endpoint and confirm both the Content-Type: application/problem+json header and the five standard members (type, title, status, detail, instance).composer show api-skeletons/laravel-api-problem (expect 2.0.1) and composer why instantiator to see who is pulling the newly-bumped dependency.ApiProblemException out of your models/services; let controllers or a small Exception → Problem mapper decide status/title/detail. That keeps HTTP semantics out of business logic and makes the problem format swappable.->render() callbacks in withExceptions() for the handful of exception types you care about. Mixing both leads to double-wrapping and confusion about which layer produced the body.ValidationException and emit 422 with an errors extension member (RFC 7807 permits extension members alongside the standard ones). Consumers then have one error parser for the whole API instead of two shapes.type as your stable, documented error code. A URI like https://api.example.com/problems/insufficient-funds gives clients something to branch on that survives copy changes to title and detail. Keep a table of them in your OpenAPI spec and treat changes as breaking.instance to the request URI (or a request/correlation id). It's the cheapest debugging win in the whole package — a support ticket with a problem body becomes traceable to a log line./api/*. Apply the middleware to the API group only; web routes should keep Laravel's HTML error pages and session-based redirects. Blanket registration produces problem+json for browser hits, which nobody expects.$response->assertStatus(422)->assertHeader('Content-Type', 'application/problem+json')->assertJsonPath('type', '...'). Assert the header explicitly — the body shape is only half the contract.$ref everywhere. Define a single Problem schema in your OpenAPI doc and reference it from every 4xx/5xx response. It keeps generated clients honest.composer update --with-all-dependencies api-skeletons/laravel-api-problem, run the suite, then re-run your API contract tests. The release is a dependency move (instantiator 2.0), not an API change, so tests passing means you're done — no code migration expected.Accept header negotiation bites. If a client sends Accept: */* or no Accept at all, Laravel's expectsJson() may return false and you'll get HTML back from paths you thought were JSON. Force the behaviour on API routes rather than relying on negotiation, and make sure your docs tell clients to send Accept: application/json.GET /api/does-not-exist — if you get HTML, handle NotFoundHttpException/MethodNotAllowedHttpException in the exception handler too.status in the body must equal the HTTP status. The single most common bug in hand-rolled implementations is a 422 header with a 500 in the body (or vice versa). Never set them in two places — derive one from the other.type must be an absolute URI per the RFC. Relative values pass in many clients and break strict ones; about:blank is the correct choice when the title is just the generic status phrase.detail user-facing and generic, and put the real exception in report()/logs. Never map $e->getMessage() from a QueryException straight into detail.type, title, status, detail, or instance. Pick a namespace (e.g. errors, trace_id) and stick to it.application/problem+json isn't in default allowed content types for some CORS setups — browsers then can't read the error body even though the request failed correctly. Add it to your CORS config.report() selectively (5xx only) or you'll drown the log.instantiator 2.0 bump is a major-version move on a dependency. If your app or another package pins instantiator ^1.x, composer update will fail with a conflict. Resolve with composer update --with-all-dependencies or align the other constraint — do not downgrade this package to force a resolution. If the new instantiator requires PHP 8.1, CI images still on 8.0 will fail at composer install; check your matrix before merging.vendor/, and re-run vendor:publish --force consciously — it will overwrite your config edits.composer.json for API-critical services and let the lockfile + contract tests, not ^ ranges, approve upgrades.How can I help you explore Laravel packages today?