Skip to content

Examples

Here are some common patterns and examples for building APIs with Koritsu.

Basic CRUD Operations

List Users

typescript
// routes/users/route.ts
export const GET = createRoute({
  method: "GET",
  handler: async ({ query }) => {
    const { limit = 10, offset = 0 } = query;
    const users = await getUsersFromDatabase({ limit, offset });
    return Response.json(users);
  },
  spec: {
    responseFormat: "json",
    tags: ["Users"],
    summary: "List users",
    parameters: {
      query: z.object({
        limit: z.coerce.number().min(1).max(100).default(10),
        offset: z.coerce.number().min(0).default(0),
      }),
    },
    responses: {
      200: { schema: z.array(userSchema) },
    },
  },
});

Get User by ID

typescript
// routes/users/[id]/route.ts
export const GET = createRoute({
  method: "GET",
  handler: async ({ params }) => {
    const user = await getUserById(params.id);
    if (!user) {
      return Response.json({ error: "User not found" }, { status: 404 });
    }
    return Response.json(user);
  },
  spec: {
    responseFormat: "json",
    tags: ["Users"],
    summary: "Get user by ID",
    parameters: {
      path: z.object({ id: z.string().uuid() }),
    },
    responses: {
      200: { schema: userSchema },
      404: { schema: errorSchema },
    },
  },
});

Create User

typescript
// routes/users/route.ts
export const POST = createRoute({
  method: "POST",
  handler: async ({ body }) => {
    const user = await createUser(body);
    return Response.json(user, { status: 201 });
  },
  spec: {
    responseFormat: "json",
    tags: ["Users"],
    summary: "Create new user",
    parameters: {
      body: createUserSchema,
    },
    responses: {
      201: { schema: userSchema },
      400: { schema: errorSchema },
    },
  },
});

Authentication Example

typescript
// routes/auth/login/route.ts
export const POST = createRoute({
  method: "POST",
  handler: async ({ body }) => {
    const { email, password } = body;
    const user = await authenticateUser(email, password);

    if (!user) {
      return Response.json({ error: "Invalid credentials" }, { status: 401 });
    }

    const token = generateJWT(user);
    return Response.json({ token, user });
  },
  spec: {
    responseFormat: "json",
    tags: ["Authentication"],
    summary: "User login",
    parameters: {
      body: z.object({
        email: z.string().email(),
        password: z.string().min(6),
      }),
    },
    responses: {
      200: {
        schema: z.object({
          token: z.string(),
          user: userSchema,
        }),
      },
      401: { schema: errorSchema },
    },
  },
});

File Upload Example

typescript
// routes/upload/route.ts
export const POST = createRoute({
  method: "POST",
  handler: async ({ body }) => {
    // Use the pre-parsed body instead of request.formData()
    const { file, description } = body;

    if (!file || !(file instanceof File)) {
      return Response.json({ error: "No file provided" }, { status: 400 });
    }

    const buffer = await file.arrayBuffer();
    const fileName = await saveFile(buffer, file.name);

    return Response.json({
      fileName,
      size: file.size,
      description: description || "No description provided"
    });
  },
  spec: {
    responseFormat: "json",
    tags: ["Files"],
    summary: "Upload file",
    parameters: {
      body: z.object({
        file: z.instanceof(File).describe("File to upload"),
        description: z.string().optional().describe("Optional file description"),
      }),
    },
    responses: {
      200: {
        schema: z.object({
          fileName: z.string(),
          size: z.number(),
          description: z.string(),
        }),\n      },\n      400: { schema: errorSchema },\n    },\n  },\n});\n```\n\n> **⚠️ Important Note about Form Data**: Always use the pre-parsed `body` parameter instead of calling `request.formData()` directly. The framework automatically handles form data parsing and calling `request.formData()` will result in a \"body already used\" error since the request stream can only be consumed once. See [Form Data and File Uploads](../core-concepts/parameters.md#form-data-and-file-uploads) for detailed examples.\n\n## Form Data Response

Form Data Response

typescript
// routes/export/route.ts
export const POST = createRoute({
  method: "POST",
  handler: async ({ body }) => {
    const { format, data } = body;

    // Generate export data
    const exportData = await generateExport(data, format);

    // Return as FormData for multi-part responses
    const form = new FormData();
    form.append(
      "file",
      new Blob([exportData], { type: "application/octet-stream" }),
      `export.${format}`
    );
    form.append(
      "metadata",
      JSON.stringify({
        exported_at: new Date().toISOString(),
        format: format,
        size: exportData.length,
      })
    );

    return new Response(form);
  },
  spec: {
    responseFormat: "formData",
    tags: ["Export"],
    summary: "Export data as form",
    parameters: {
      body: z.object({
        format: z.enum(["csv", "json", "xlsx"]),
        data: z.array(z.record(z.any())),
      }),
    },
    responses: {
      200: {
        description: "Export data as multipart form",
        headers: {
          "Content-Type": {
            description: "multipart/form-data",
            schema: { type: "string" },
          },
        },
      },
      400: { schema: errorSchema },
    },
  },
});

Health Check

typescript
// routes/health/route.ts
export const GET = createRoute({
  method: "GET",
  handler: async () => {
    const health = {
      status: "healthy",
      timestamp: new Date().toISOString(),
      uptime: process.uptime(),
    };
    return Response.json(health);
  },
  spec: {
    responseFormat: "json",
    tags: ["System"],
    summary: "Health check",
    responses: {
      200: {
        schema: z.object({
          status: z.string(),
          timestamp: z.string(),
          uptime: z.number(),
        }),
      },
    },
  },
});