Skip to content

Results

A handler returns a result descriptor. The runtime serializes that descriptor into an HTTP response.

ts
import { Results } from "sloppy";

app.get("/", () => Results.text("ok"));
app.get("/users", () => Results.ok([{ id: 1 }]));
app.get("/oops", () => Results.problem({ status: 500, title: "Boom" }));

Every helper is on the Results namespace.

JSON results

HelperStatusContent-Type
Results.ok(value, options?)200application/json; charset=utf-8
Results.created(loc, value, opts?)201application/json; charset=utf-8, Location: <loc>
Results.accepted(value, options?)202application/json; charset=utf-8
Results.notFound(value?, options?)404application/json; charset=utf-8
Results.badRequest(value?, opts?)400application/json; charset=utf-8
Results.unauthorized(value?, opts?)401application/json; charset=utf-8
ts
return Results.ok({ id: 1, name: "Ada" });
return Results.created(`/users/${user.id}`, user);

Results.notFound() and Results.badRequest() accept an optional payload. Called with no argument, the descriptor carries no body — the runtime sends only headers. With a string they emit "…"; with an object, the object.

JSON payloads use Sloppy's alpha JSON policy before response bytes are emitted:

  • Plain objects and arrays serialize recursively.
  • Strings, finite numbers, booleans, and null serialize as their JSON values.
  • Date serializes as an ISO 8601 string.
  • BigInt serializes as a decimal string by default.
  • Uint8Array, ArrayBuffer, and typed-array views serialize as base64 strings.
  • undefined object fields are omitted; undefined array entries become null.
  • Circular references, symbols, functions, invalid dates, and NaN/infinite numbers throw.
  • Class instances serialize their enumerable own properties.
  • Error objects serialize as { name, message } plus enumerable safe fields; stack is not included.

For compiled/native artifacts, literal/static Results.json(...) and Results.ok(...) values can be emitted as native static JSON response metadata. Those routes bypass JavaScript at request time and the native response writer copies the preencoded JSON bytes with deterministic Content-Type and Content-Length handling. Dynamic handler return values still use the generic JSON serializer unless the Plan records a supported native response mode. Routes with response metadata that cannot be written natively carry an explicit jsonResponse.fallbackReason in the Plan and in sloppy routes --dispatch.

Use app-level JSON options when you need the small supported policy switches:

ts
import { Sloppy } from "sloppy";

const app = Sloppy.create({
    json: {
        casing: "camelCase",
        includeNulls: true,
        dateFormat: "iso8601",
        bigint: "string",
    },
});

app.useJson({ casing: "camelCase", bigint: "string" }) applies the same options before the app is frozen. Current options are casing: "preserve" | "camelCase", includeNulls: boolean, dateFormat: "iso8601", bigint: "string" | "error", and bytes: "base64" | "array".

Body-shape results

HelperStatusContent-Type
Results.text(body, options?)200text/plain; charset=utf-8
Results.json(value, options?)200application/json; charset=utf-8
Results.html(body, options?)200text/html; charset=utf-8
Results.bytes(body, options?)200application/octet-stream
Results.stream(writer, options?)200application/octet-stream by default

body for text and html is a string. For bytes, it's a Uint8Array (or any BufferSource).

Results.stream(async writer => { ... }) builds a bounded stream descriptor:

ts
return Results.stream(async (writer) => {
    writer.writeText("hello ");
    writer.writeBytes(new Uint8Array([119, 111, 114, 108, 100]));
}, { contentType: "text/plain; charset=utf-8" });

The JavaScript callback writes into a bounded descriptor before the handler returns. The native response writer validates that descriptor, computes Content-Length for bounded serialization, and preserves that length for HEAD responses. The HTTP/1.1 transport lowers the descriptor into a Core readable stream and emits bounded chunked frames with pending-write and response caps.

Native JSON responses use the same HTTP response writer as other fixed responses. Current native static JSON writing is bounded and preencoded; live incremental JSON writer state is not exposed as a public streaming surface.

The public JS surface is still a descriptor builder, not live handler push, incremental file send, WHATWG Streams, or Node streams.

Realtime.sse(...) uses this bounded stream descriptor path and sets text/event-stream headers for SSE frames. It does not add live incremental flush guarantees beyond Results.stream.

No-content

ts
return Results.noContent();   // 204

Custom status

ts
return Results.status(202, { jobId: "x" });
return Results.status(418);                    // empty body

Results.status(code, value?, options?) is the escape hatch for any status code. With a value, it serializes as JSON.

Problem details

ts
return Results.problem({
    status: 409,
    title: "User already exists",
    detail: "A user with that email is already registered.",
    code: "USER_ALREADY_EXISTS",
});

Returns application/problem+json; charset=utf-8 with the supplied fields. A bare string becomes the title of a 500 (with no detail unless you pass an object):

ts
return Results.problem("Database is down");
// → { title: "Database is down", status: 500 }

Calling Results.problem() with no argument produces { title: "Sloppy problem", status: 500 }.

Cache Headers

Result descriptors can attach HTTP cache policy headers without enabling server-side output caching:

ts
return Results.json({ theme: "dark" })
    .cacheControl("private, max-age=30")
    .cacheHeaders({
        vary: ["Accept-Language"],
        etag: true,
    });

cacheControl(value) sets Cache-Control. cacheHeaders(options) can set cacheControl, merge Vary, set Last-Modified, and generate or set ETag. Use route .outputCache(...) when Sloppy should store the server response.

Default error responses

Install ProblemDetails.defaults() once on the app to wrap every route in a catch that returns a safe 500 application/problem+json body whenever a handler throws or rejects:

ts
import { Sloppy, ProblemDetails, Results } from "sloppy";

const app = Sloppy.create();
app.use(ProblemDetails.defaults());

app.get("/boom", () => { throw new Error("internal failure"); });

The thrown /boom handler returns:

json
{"status":500,"title":"Internal Server Error","code":"SLOPPY_E_HANDLER_ERROR"}

The default body never includes the exception message. Pass ProblemDetails.defaults({ detail: "development" }) to include it only when Sloppy:Environment is Development, or { detail: "always" } to include it in every environment. Explicit Results.problem(...) descriptors are passed through untouched.

Validation failures from ctx.body.validate(schema) are mapped separately to 400 application/problem+json with code SLOPPY_E_VALIDATION_FAILED.

Options

Every helper takes a final options object:

ts
return Results.ok(data, {
    status: 207,                        // override the default status
    headers: { "x-trace": traceId },    // extra response headers
    contentType: "application/vnd.foo+json",
});
  • status — override the helper's default status code.
  • headers — extra response headers. Names must be valid HTTP tokens; values must be strings without control characters (horizontal tab is allowed). Runtime-managed headers — Content-Type, Content-Length, Connection, Transfer-Encoding, Keep-Alive — are rejected; set contentType instead. Invalid names or values throw a TypeError.
  • contentType — override the helper's default Content-Type. The runtime still adds ; charset=utf-8 for textual content where appropriate.
  • json — per-result JSON options for the supported Sloppy JSON policy.

Every descriptor also has .cookie(name, value, options?):

ts
return Results.ok({ ok: true })
    .cookie("session", sessionId, {
        httpOnly: true,
        secure: true,
        sameSite: "Strict",
        path: "/",
        maxAge: 3600,
    });

Cookie options are path, domain, maxAge, expires, httpOnly, secure, and sameSite ("Strict", "Lax", or "None"). Each call appends a separate Set-Cookie header.

Async handlers

Handlers can return a Promise<Result>. The runtime awaits it during the owner-thread microtask drain — long-running awaits aren't supported yet.

ts
app.get("/users/{id:int}", async (ctx) => {
    const user = await loadUser(ctx.route.id);
    return user ? Results.ok(user) : Results.notFound();
});

If the promise rejects or the handler throws, the runtime returns 500 with a redacted diagnostic body.

Public alpha. APIs and artifact formats may still change between alpha revisions.