Add a JSON endpoint#
Add a POST /articles endpoint that accepts JSON and returns the submitted fields with 201 Created. If the endpoint exposes durable records, combine it with recipes/add-database-backed-resource.
Accept JSON and return a response#
Create src/actions/articles.zig, keeping the endpoint-specific input beside its action:
// src/actions/articles.zig
const zyn = @import("zyn");
const Payload = struct {
title: []const u8,
published: bool = false,
};
pub fn create(action: *zyn.Action) !zyn.Response {
const payload = try action.expect_payload(Payload);
return action.json(.{
.title = payload.title,
.published = payload.published,
}, .{ .status = .created });
}
Payload requires a title and defaults published to false. The action returns those two fields as JSON with 201 Created, without storing a record. Let error.InvalidRequestPayload from expect_payload propagate unless the application intentionally owns a different invalid-input response.
Register the route#
Import the action module in src/routes.zig and add the POST /articles entry to its existing table. Keep the module's existing routes and defaults:
// src/routes.zig
const zyn = @import("zyn");
const articles = @import("actions/articles.zig");
pub const table: zyn.RouteTable = &.{
// Keep existing route entries here.
.post("/articles", articles.create, &.{}),
};
The application must export the route module from src/app.zig. Generated applications already contain this declaration:
// src/app.zig
pub const routes = @import("routes.zig");
Defining an action does not register a route. The application dispatches through the exported route module's table. If the route has a module or mount prefix, include that prefix in requests; zyn routes shows the effective path.
Keep the representation explicit#
Define a reusable public representation in one TitleCase module under src/resources. Do not serialize generated database row types; copy or query only the fields that belong in the application-owned representation. Keep passwords, authorization values, protected cookies, tokens, and other secrets out of responses and logs.
For a custom wire format, put a parse function on the payload type and use payload.invalidate for recoverable field failures. Keep the parser beside the action when it is endpoint-specific and beside the resource when it is shared.
Apply route security#
Attach any guards or hooks needed to authenticate callers and authorize access when registering the route.
Unsafe browser requests under .csrf(.protected) must satisfy the route's Fetch Metadata policy. Exempt only endpoints that enforce an appropriate independent authenticity policy, such as a verified webhook signature.
Verify#
zyn routes
zyn check
For a root-level route, zyn routes should list POST /articles. Start the development server in HTTP mode:
zyn dev --http
In another terminal, send a JSON request to its default port:
curl -i http://localhost:7532/articles \
-H 'Content-Type: application/json' \
-H 'Sec-Fetch-Site: same-origin' \
--data '{"title":"Example article"}'
Expect 201 Created, a JSON content type, and {"title":"Example article","published":false}. Use the server's reported port if PORT overrides the default.
Test at least the successful representation, missing or invalid fields, unknown fields when relevant, authorization failure, and the endpoint's actual status and content type. Managed HTTP tests for unsafe methods need a valid Sec-Fetch-Site field when CSRF protection applies.