chriskelemba / jsonapi-response
JSON:API response builder for Laravel
Requires
- php: ^8.2
- illuminate/http: ^12.0
- illuminate/pagination: ^12.0
- illuminate/support: ^12.0
README
A lightweight JSON:API response helper for Laravel with pagination, links, query helpers (sort/filter/include/fields), and consistent response formatting.
Install
composer require chriskelemba/jsonapi-response
Laravel package auto-discovery will register the service provider automatically.
Publish config (optional)
php artisan vendor:publish --tag=jsonapi-config
You do not need to publish config to use the package.
Quick Start
Collection + Pagination
use ChrisKelemba\ResponseApi\JsonApi; return JsonApi::response(Book::query(), 'books', request());
Passing a Builder or Relation now auto-paginates by default using ?page[number] and ?page[size].
To override page size:
- Per request:
?page[size]=25 - Default for all requests: set
pagination.default_sizeinconfig/jsonapi.php
To omit pagination for a query response:
- Per request:
?page[disable]=true(or?page[size]=0) - Globally: set
pagination.enabledtofalse - Per endpoint: call
->get()and pass the collection toJsonApi::response()
Drop-in Response Replacement
Keep your existing controller logic and only replace the return line:
$books = Book::all(); return JsonApi::response($books, 'books', request());
Single Resource
return JsonApi::response($book, 'books', request());
Create (201 + Location header)
$book = Book::create($validated); return JsonApi::response($book, 'books', null, 201);
Delete (204)
return JsonApi::response(null, null, null, 204);
Query Helpers (JSON:API recommendations)
Apply sort, filter, include, and sparse fieldsets using a safe allow‑list.
$query = JsonApi::applyQuery(Book::query(), request(), 'books', [ 'allowed_sorts' => ['created_at', 'title'], 'allowed_filters' => ['author', 'genre'], 'allowed_includes' => ['author'], 'allowed_fields' => ['title', 'author', 'created_at'], ]); $books = $query->paginate(15); return JsonApi::response($books, 'books', request());
Minimal Controller Setup
For low-boilerplate controllers, use model-aware helpers:
use App\Models\User; use ChrisKelemba\ResponseApi\JsonApi; use Illuminate\Http\Request; public function index(Request $request) { $query = JsonApi::applyModelQuery(User::query(), $request, 'users', [ 'allowed_includes' => ['tasks', 'roles', 'permissions'], ]); $users = JsonApi::paginateQuery($query, $request); return JsonApi::response($users, 'users', $request); }
applyModelQuery() auto-derives allow-lists from the model and excludes hidden attributes.
Supported query params:
?sort=created_ator?sort=-created_at?filter[author]=Jane?include=author,comments?max_include=100?max_include[comments]=50?max_include[tasks.subTasks]=5?fields[books]=title,author?page[number]=2&page[size]=25
You can also pass the query directly to response() and let the package paginate automatically:
$query = JsonApi::applyModelQuery(User::query(), $request, 'users'); return JsonApi::response($query, 'users', $request);
Relationships + Included
If a relationship is loaded, the package will add a JSON:API relationships object automatically.
To load and include related resources without controller code, use ?include=:
GET /api/book-authors?include=books
Nested includes are supported via dot notation:
GET /api/users/1?include=tasks.subTasks
This will:
- Load
booksautomatically. - Add
relationships.booksto each author. - Add
includedresources for the related books (unlessinclude_compound_documentsis disabled). - If
relationships.links_for_includesis enabled, relationship links are emitted even when the relation isn't loaded.
Limit Included Size
If a relationship is large, you can cap how many related resources are serialized in relationships and included:
GET /api/book-authors/51?include=books&max_include=100
Nested per-path include limits are supported:
GET /api/users/1?include=tasks.subTasks&max_include[tasks]=10&max_include[tasks.subTasks]=3
Notes:
- This limits the response size only. It does not change how many related rows are loaded from the database.
- For large related sets, prefer paging the related collection via a filtered endpoint.
- There is no default include cap unless you set
query.max_includeor passmax_includein the request.
Relationship Pagination (JSON:API standard)
include is for sideloading, not true paging of related collections.
Use the related endpoint for paging:
GET /api/book-authors/51/books?page[number]=1&page[size]=10
Pagination Links Preserve Query
Pagination links (first, next, last) keep your current query parameters (e.g. filter, include, fields).
Example (controller stays simple):
public function index(Request $request) { $authors = BookAuthor::query()->paginate(15); return JsonApi::response($authors, 'book-authors', $request); }
Error Responses
use ChrisKelemba\ResponseApi\JsonApi; $errors = [ JsonApi::error( 422, 'Validation Error', 'The title field is required.', 'VALIDATION_ERROR', ['pointer' => '/data/attributes/title'], ['field' => 'title'], 'err_123', ['about' => 'https://api.example.com/errors/err_123'] ), ]; return JsonApi::responseErrors($errors, 422);
Supported error members include id, links, status, code, title, detail, source, and meta.
Validation Errors
use ChrisKelemba\ResponseApi\JsonApi; return JsonApi::responseValidationErrors($validator->errors());
Configuration
All defaults are in config/jsonapi.php.
Key options:
transform_keys: Enforce JSON:API member naming recommendations (camelCase + ASCII)transform_recursive: Apply key transform to nested payload structurestransform_attributes: Transform keys inside resourceattributes(setfalseto keep snake_case likeparent_id)attributes.include_custom_primary_key: Keep custom primary key columns inattributes(e.g.ctg_id) while still sending resourceidresource_links/relationship_links: Include resource/relationship linkslinks.resource_base_path: Prefix generated resource/relationship links (default:api)method_override: EnableX-HTTP-Method-Override: PATCHinclude_jsonapi: Include or suppress the top-leveljsonapiobjectinclude_compound_documents: Include or suppress top-levelincludedeager_load_includes: Toggle eager loading for?include=relationships.links_for_includes: Emit relationship links for?include=even if not loadedquery.*: Allow‑lists for sort/filter/include/fieldsquery.allow_all_filters: Allow anyfilter[...]key without an allow‑list (useful for dynamic APIs)query.allow_all_sorts: Allow anysortfield without an allow‑listquery.allow_all_includes: Allow anyincludewithout an allow‑listquery.allow_all_fields: Allow anyfields[...]without an allow‑listerrors.include_all_members: Force error objects to include all standard memberspagination.enabled: Auto-paginateBuilder/Relationpayloads inJsonApi::response()pagination.allow_disable/pagination.disable_param: Allow request-level opt-out (e.g.page[disable]=true)pagination.number_param/pagination.size_param: Page parameter names (page.number/page.sizeby default)pagination.default_size/pagination.max_size: Default and max page size limits
Notes
- Content-Type is set to
application/vnd.api+json. - ISO‑8601 timestamps are recommended.
- Eloquent hidden attributes are respected in serialized
attributes(e.g.password,remember_token).
License
MIT