Suprnova ships a JSON:API resource layer for typed REST APIs. Mark a
#[derive(Data)] struct with #[json_resource("type")] and the framework
emits an IntoJsonResource impl that handles single envelopes, collections,
paginated collections, sparse fieldsets (?fields[type]=...), compound
included documents, and multi-level ?include=a.b.c chains through the
same code path. The two facades - Resource and JsonApi - are the same
type under two names; use whichever matches your house style.
Defining a resource
use Data;
The id_field keyword renames the field that supplies the JSON:API id:
Rendering responses
Construct a pending response from a handler and call .render().await:
use ;
async
async
async
JsonApi::single / JsonApi::collection / JsonApi::paginated are
identical alias entry points if you prefer the Laravel spelling.
Chainable mutators
JsonApiResponse is a pending object. Customise the envelope before
calling .render().await. Every mutator is self → Self so they
compose:
use ;
use json;
let info = new
.with_version
.with_ext
.with_meta;
single
.status // HTTP status override
.with_meta // top-level meta KV
.with_link // top-level link
.with_jsonapi // top-level `jsonapi`
.additional
.render
.await
| Mutator | Laravel analogue | Effect |
|---|---|---|
.status(code) |
ResourceResponse::calculateStatus |
Overrides HTTP status. |
.created() |
wasRecentlyCreated → 201 |
Shorthand for .status(201). |
.with_meta(k, v) / .meta(k, v) |
with($request) |
Top-level meta KV. |
.with_meta_map(m) |
bulk with($request) |
Merge a map into top-level meta. |
.with_link(rel, href) / .link(rel, href) |
with($request)['links'] |
Top-level links KV. |
.with_link_value(rel, v) |
link-object form | Top-level link as {href, meta}. |
.with_additional(k, v) |
additional($data) |
Root-level key alongside data. |
.additional(map) |
additional($data) |
Bulk additional keys. |
.with_jsonapi(info) |
JsonApiResource::configure(...) |
Top-level jsonapi member. |
Canonical members (data, included, links, meta, jsonapi,
errors) are never overwritten by .additional(...).
Per-resource links and meta
Override the IntoJsonResource::resource_links and
IntoJsonResource::resource_meta defaults to attach links / metadata
to the resource object, not the document root:
use IntoJsonResource;
use ;
Both default to an empty Map for macro-derived resources, so the
JSON:API renderer omits the keys when not used. Override
resource_top_level_meta to lift per-resource metadata into the
envelope's top-level meta member.
Conditional attributes - Maybe<T> / MissingValue<T>
Use Maybe to omit a field from the rendered attributes object based
on a runtime condition. This is the Suprnova analogue of Laravel's
MissingValue and the when() / whenLoaded() / unless() family.
use ;
// Both names point at the same type.
let m1: = present;
let m2: = missing;
let m3 = when;
let m4 = unless;
let m5 = when_with; // lazy
For macro-derived structs, declare a field as Maybe<T> and the
renderer drops it automatically when Missing. For hand-rolled
resource_attributes, use the insert_maybe(map, key, maybe) helper:
use ;
The renderer also calls strip_missing_values(&mut value) over the
entire attributes object, so Maybe::Missing values nested inside
arbitrary serde-derived structures are dropped recursively - useful
when a deeply-nested transformer wants to omit subfields.
Sparse fieldsets
The framework's IncludeMiddleware parses
?fields[type]=email,name-style query parameters and binds them to a
task-local. The macro-emitted resource_attributes consults the
fieldset and only emits requested attributes. No handler-side work is
needed - install the middleware and the resource layer honours it
automatically.
// Request: GET /api/users/7?fields[users]=email
// Response: { "data": { "type": "users", "id": "7", "attributes": { "email": "alice@example.com" } } }
Compound documents - ?include= chains
Declare relationship fields with #[data(allow_include)]. The framework
builds an IncludeTree from ?include=author.posts.tags,comments, walks
every node, and pushes fully-resolved resource objects into included.
Deduplication runs at push time through IncludedSink, keyed by
(type, id) per JSON:API spec §8 - so a 1,000-item collection where
every item shares the same author resolves the author exactly once. Peak
memory and CPU stay proportional to the distinct included resources,
not the relationship fan-in.
A request that names an include path not on this resource's allowlist gets a JSON:API 400 errors envelope.
Why Suprnova diverges
Two visible divergences from Laravel's JsonApiResource:
-
Strict default-deny for
?include=. Laravel's resource layer silently ignores include paths that don't resolve. Suprnova rejects them with a400 Bad Requestcarrying a JSON:API errors envelope. The spec's §5.2.2 default-deny posture is the contract clients can program against; silent ignore hides client bugs and breaks compound-document integrity. -
Explicit
.status(code)/.created()instead of auto-201. Laravel auto-sets201fromwasRecentlyCreatedon the underlying Eloquent model. Suprnova decouples the resource DTO from any specific persistence lifecycle, so the status is set on the response object itself -.created()when you mean it,.status(204)when the response is empty, and so on. A single mutator stays honest under any flow.
Pagination
Resource::paginated(p) works with any paginator implementing the
Paginated<T> trait - both LengthAwarePaginator<T> and
CursorPaginator<T> from suprnova::pagination ship this impl. The
renderer attaches links.{self,first,prev,next,last} and a
meta.pagination block automatically.
use ;
let page = new
.with_base_url;
paginated.render.await
Error envelopes
Every FrameworkError knows how to render itself as a JSON:API
{"errors": [...]} envelope via into_json_api_response(). The
helper is exposed because FrameworkError carries a status code, a
field-name source pointer (for ValidationError), and a request-id
correlation token under meta.request_id. 5xx responses are
sanitised: the raw message never reaches the client unless
APP_DEBUG=true is set in the active environment, in which case it
appears under meta.debug_message.
let response = validation
.into_json_api_response;
// {
// "errors": [{
// "status": "422",
// "title": "Validation failed",
// "detail": "email is invalid",
// "source": { "pointer": "/data/attributes/email" },
// "meta": { "request_id": "..." }
// }]
// }
Surfaces summary
| Suprnova surface | Laravel 13 equivalent |
|---|---|
Resource / JsonApi facades |
JsonResource::make, JsonApiResource |
JsonApiResponse |
ResourceResponse, JsonApiResource::toResponse |
JsonApiBuilder |
(internal builder for ResourceResponse) |
IntoJsonResource trait |
JsonResource::toArray, toAttributes, toRelationships, toLinks, toMeta, with |
RelationshipValue / ResourceIdentifier |
array shape inside toRelationships |
IncludeTree |
parsed ?include= from JsonApiRequest |
RequestFieldsetSet |
parsed ?fields[type]= from JsonApiRequest |
Maybe<T> / MissingValue<T> |
MissingValue + whenLoaded / when / unless |
JsonApiInfo |
JsonApiResource::$jsonApiInformation |
JsonApiResponse::status(code) / .created() |
ResourceResponse::calculateStatus |
JsonApiResponse::additional(map) / .with_additional(k, v) |
JsonResource::additional($data) |
JsonApiResponse::with_meta(k, v) / .meta(k, v) |
JsonResource::with($request)['meta'] |
JsonApiResponse::with_link(rel, href) / .link(rel, href) |
JsonResource::with($request)['links'] |
JsonApiResponse::with_jsonapi(info) |
JsonApiResource::configure(...) |
current_fieldset() / scope_fieldset(...) |
task-local fieldset, set by IncludeMiddleware |
IncludeResolutionError → 400 envelope |
strict-mode ?include= parser |
Top-level re-exports under suprnova::: Resource, JsonApi,
JsonApiResponse, JsonApiBuilder, JsonApiInfo, IncludedSink,
IntoJsonResource, RelationshipValue, ResourceIdentifier,
IncludeTree, RequestFieldsetSet, Maybe, MissingValue,
insert_maybe, strip_missing_values, AsRelationshipValue,
PushIncluded, IncludeResolutionError, current_fieldset,
scope_fieldset.
Next
- Eloquent serialization -
#[derive(Data)], hidden/visible fields, thetoArrayequivalent that feeds resource attributes - Eloquent relationships - what
#[data(allow_include)]consumes; the typed relation kinds backing compound documents - Pagination -
LengthAwarePaginator,CursorPaginator, and thePaginated<T>traitResource::paginatedconsumes - Data - the
#[derive(Data)]macro shared with Inertia, the?include=/?fields[type]=middleware, andMaybe<T>patterns - Error model - how
FrameworkError::into_json_api_responsefits the conversion contract
