Fragno
Features

Route Definitions

Learn how to define routes in Fragno

Edit on GitHub

The defineRoute function is the core building block for creating API endpoints. It provides a type-safe way to define HTTP routes with validation, error handling, and automatic type inference.

Basic Route Structure

import { defineRoute } from "@fragno-dev/core";

const route = defineRoute({
  method: "GET",
  path: "/users",
  handler: async ({ query }, { json }) => {
    return json({ users: [] });
  },
});

Path Configuration

Valid Paths

Paths must follow these rules:

  • Start with / (cannot be empty or missing leading slash)
  • Cannot be just / (root path)
  • Cannot end with /

Example paths:

"/users"; // Basic path
"/v1/users"; // Nested path
"/users/:id"; // With parameter
"/files/**:path"; // With wildcard

By default, paths are mounted under /api/<fragment-name>. This means that when you define the /users route, it will be mounted under /api/<fragment-name>/users.

Path Parameters

Use :name for single-segment parameters and **:name for wildcard parameters that capture the entire remainder of the path.

// Single parameter
defineRoute({
  method: "GET",
  path: "/users/:id",
  handler: async ({ pathParams }, { json }) => {
    // pathParams is typed as { id: string }
    return json({ userId: pathParams.id });
  },
});

// Multiple parameters
defineRoute({
  method: "GET",
  path: "/organizations/:orgId/users/:userId",
  handler: async ({ pathParams }, { json }) => {
    // pathParams is typed as { orgId: string; userId: string }
    return json({ orgId: pathParams.orgId, userId: pathParams.userId });
  },
});

// Wildcard parameter
defineRoute({
  method: "GET",
  path: "/files/**:filepath",
  handler: async ({ pathParams }, { json }) => {
    // pathParams is typed as { filepath: string }
    // Captures entire remainder: "/files/docs/readme.txt" -> filepath = "docs/readme.txt"
    return json({ file: pathParams.filepath });
  },
});

Input Schema

Define input validation using any Standard Schema compatible library, such as Zod:

import { z } from "zod";

defineRoute({
  method: "POST",
  path: "/users",
  inputSchema: z.object({
    name: z.string().min(1),
    email: z.string().email(),
    age: z.number().optional(),
  }),
  handler: async ({ input }, { json }) => {
    // input.valid() returns the validated data
    const userData = await input.valid();
    // userData is typed as { name: string; email: string; age?: number }

    return json({ id: "123", ...userData });
  },
});

The input context provides:

  • input.valid() - Async method that validates and returns the parsed data. This method throws a FragnoApiValidationError if the input is invalid, which is automatically converted to a 400 response.
  • input.schema - The schema object

Output Schema

Define the expected response structure for type safety and documentation:

const UserSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string(),
  createdAt: z.string(),
});

defineRoute({
  method: "GET",
  path: "/users/:id",
  outputSchema: UserSchema,
  handler: async ({ pathParams }, { json }) => {
    const user = await getUserById(pathParams.id);
    // TypeScript ensures the return value matches UserSchema
    return json(user);
  },
});

Output Schema when Streaming

When using streaming responses with jsonStream, the output schema object is expected to be an array of the items you want to stream.

Error Codes

Error codes can be defined on a route as a hint to the client. These codes serve as hints for forward-compatibility reasons. Error codes are expected to be strings, as updating a Fragment might introduce new error codes.

defineRoute({
  method: "POST",
  path: "/users",
  inputSchema: z.object({ email: z.string().email() }),
  errorCodes: ["USER_EXISTS", "VALIDATION_ERROR"],
  handler: async ({ input }, { json, error }) => {
    const { email } = await input.valid();

    if (await userExists(email)) {
      return error(
        { message: "User already exists", code: "USER_EXISTS" },
        409, // HTTP status code
      );
    }

    return json({ id: "123", email });
  },
});

Error responses are returned to the client as:

{
  "message": "User already exists",
  "code": "USER_EXISTS"
}

Query Parameters

Like error codes, query parameters are just a hint. The query object is a web-standard URLSearchParams object.

defineRoute({
  method: "GET",
  path: "/users",
  queryParameters: ["page", "limit", "sort"],
  handler: async ({ query }, { json }) => {
    const page = query.get("page") || "1";
    const limit = query.get("limit") || "10";
    const sort = query.get("sort");

    // Access any query parameter via query.get()
    const users = await getUsers({ page, limit, sort });
    return json(users);
  },
});

Handler Callback

The route handler receives two context objects that provide access to request data and response methods.

Request Input Context

The first parameter provides access to all incoming request data. Available properties are:

Prop

Type

The input property is only defined if an input schema is defined for the route.

Prop

Type

Request Output Context

The second parameter provides methods for creating responses.

JSON Responses

Create JSON responses with automatic serialization and proper content-type headers:

// Basic JSON response (200 status)
return json({ message: "Success", data: [1, 2, 3] });

// JSON with custom status code
return json({ id: "123", created: true }, 201);

// JSON with custom status and headers
return json(
  { result: "ok" },
  {
    status: 200,
    headers: { "X-Custom": "value" },
  },
);

// JSON with just status number
return json({ count: 42 }, 200);

Empty Responses

Create responses with no body content:

// Empty response (201 status by default)
return empty();

// Empty with custom status
return empty(204);

// Empty with custom status and headers
return empty({
  status: 202,
  headers: { Location: "/new-resource" },
});

Error Responses

Create structured error responses with consistent formatting:

// Basic error (500 status by default)
return error({ message: "Internal error", code: "INTERNAL_ERROR" });

// Error with custom status
return error({ message: "Not found", code: "NOT_FOUND" }, 404);

// Error with custom status and headers
return error(
  { message: "Rate limited", code: "RATE_LIMITED" },
  {
    status: 429,
    headers: { "Retry-After": "60" },
  },
);

// Error with just status number
return error({ message: "Bad request", code: "BAD_REQUEST" }, 400);

Streaming Responses

return jsonStream(async (stream) => {
  for await (const { progress } of eventStream) {
    stream.write({ type: "progress", value: progress });
  }

  // Send final result
  stream.write({ type: "complete", result: "Processing finished" });
});

The streaming response uses Content Type application/x-ndjson (newline-delimited JSON). The parameter of the write method is expected to be an array element of the output schema. As such, errors can be defined in-line.

Response Method Signatures

// JSON response
json(data: TOutput, initOrStatus?: ResponseInit | StatusCode, headers?: HeadersInit): Response

// Empty response
empty(initOrStatus?: ResponseInit<ContentlessStatusCode> | ContentlessStatusCode, headers?: HeadersInit): Response

// Error response
error(
  { message: string; code: TErrorCode },
  initOrStatus?: ResponseInit | StatusCode,
  headers?: HeadersInit
): Response

// Streaming response
jsonStream(
  callback: (stream: ResponseStream<TOutput>) => void | Promise<void>,
  options?: {
    onError?: (error: Error, stream: ResponseStream<TOutput>) => void | Promise<void>;
    headers?: HeadersInit;
  }
): Response