API Resources
An API resource is a dedicated transformer between your domain object and the JSON map you send to clients. Keeping that mapping in one class avoids leaking internal fields and keeps list vs detail payloads consistent.
Generate a stub with ./grind make:resource User (--collection adds a collection helper).
Single resource
Section titled “Single resource”Subclass JsonResource and implement toArray. By default resolve / toResponse wrap the map under a "data" key. Call JsonResource.withoutWrapping() (usually once at boot) to disable that, or JsonResource.wrap("payload") to rename it.
class UserResource( private val id: Int, private val name: String, private val email: String?, model: Any? = null,) : JsonResource(model) { override fun toArray(request: Request): Map<String, Any?> = mapOf( "id" to id, "name" to name, "email" to `when`(email != null, email), "admin" to mergeWhen(request.header("X-Role") == "admin", mapOf("role" to "admin")), "posts" to whenLoaded("posts"), )}
router.get("/users/{id}") { request -> UserResource(1, "Ada", "ada@example.com").toResponse(request)}public class UserResource extends JsonResource { private final int id; private final String name;
public UserResource(int id, String name, Object model) { super(model); this.id = id; this.name = name; }
@Override public Map<String, Object> toArray(Request request) { Map<String, Object> data = new HashMap<>(); data.put("id", id); data.put("name", name); data.put("posts", whenLoaded("posts")); return data; }}
router.get("/users/{id}", request -> new UserResource(1, "Ada", null).toResponse(request));Conditional helpers
Section titled “Conditional helpers”| Helper | Behavior |
|---|---|
when(condition, value) |
Omit the key when the condition is false (MissingValue) |
mergeWhen(condition, map) |
Flatten map into the parent when true |
whenLoaded("relation") |
Include a relation only if the underlying model already loaded it (relationLoaded / getRelation — works with Cadence models without a hard dependency) |
additional(map) / with(request) |
Extra top-level keys beside the wrapped payload |
toResponse wraps resolve in Response.json(...). Call resolve(request) when you need the map without building a response yet.
Collections
Section titled “Collections”Wrap many resources with JsonResource.collection(...). By default the payload is { "data": [ ... ] }. Change or remove the wrapper with wrap:
val payload = JsonResource.collection( listOf(UserResource(1, "Ada", null), UserResource(2, "Grace", null)),).wrap("users").toResponse(request)Response payload = JsonResource.collection( List.of(new UserResource(1, "Ada", null), new UserResource(2, "Grace", null))).wrap("users").toResponse(request);Pass wrap(null) for a bare JSON array.
With API routes
Section titled “With API routes”Register an API resource controller, then return resources from actions:
router.group(prefix = "/api", name = "api.") { apiResource("projects", ProjectApiController(app))}Typical flow: controller loads a model (optionally with relations) → builds a resource → returns toResponse. Pair with Controllers, resource / apiResource routes, and named links from URL generation.