Skip to main content
Get up and running with Semola by building a type-safe REST API in just a few steps.
1

Install Semola

Install Semola using your preferred package manager:
bun add semola
Semola requires Bun 1.1+ as the runtime. Install Bun from bun.sh
2

Install a validation library

Semola uses Standard Schema for validation. Install Zod (recommended) or your preferred validator:
bun add zod
Other supported validators: Valibot, ArkType, or any Standard Schema v1 compatible library.
3

Create your first API

Create a new file server.ts and build a type-safe REST API:
server.ts
import { Api } from "semola/api";
import { z } from "zod";

const api = new Api();

// Define a type-safe route
api.defineRoute({
  path: "/hello/:name",
  method: "GET",
  request: {
    params: z.object({ name: z.string() }),
  },
  response: {
    200: z.object({ message: z.string() }),
  },
  handler: async (ctx) => {
    return ctx.json(200, { 
      message: `Hello, ${ctx.params.name}!` 
    });
  },
});

// Start the server
api.serve(3000, () => {
  console.log("Server running on http://localhost:3000");
});
Run your server:
bun server.ts
Test it:
curl http://localhost:3000/hello/world
{
  "message": "Hello, world!"
}
4

Add request body validation

Create a POST endpoint with body validation:
api.defineRoute({
  path: "/users",
  method: "POST",
  request: {
    body: z.object({
      name: z.string().min(1),
      email: z.string().email(),
      age: z.number().min(0).optional(),
    }),
  },
  response: {
    201: z.object({
      id: z.string(),
      name: z.string(),
      email: z.string(),
    }),
    400: z.object({
      message: z.string(),
    }),
  },
  handler: async (ctx) => {
    // ctx.req.body is fully typed!
    const user = {
      id: crypto.randomUUID(),
      name: ctx.req.body.name,
      email: ctx.req.body.email,
    };
    
    return ctx.json(201, user);
  },
});
Notice how TypeScript automatically infers types from your Zod schemas. No manual type annotations needed!
5

Handle errors gracefully

Use Semola’s result-based error handling:
import { mightThrow } from "semola/errors";

api.defineRoute({
  path: "/fetch-data",
  method: "GET",
  response: {
    200: z.object({ data: z.any() }),
    500: z.object({ error: z.string() }),
  },
  handler: async (ctx) => {
    const [error, response] = await mightThrow(
      fetch("https://api.example.com/data")
    );
    
    if (error) {
      return ctx.json(500, { 
        error: "Failed to fetch data" 
      });
    }
    
    const [parseError, data] = await mightThrow(response.json());
    
    if (parseError) {
      return ctx.json(500, { 
        error: "Failed to parse response" 
      });
    }
    
    return ctx.json(200, { data });
  },
});
No try-catch blocks needed! The [error, data] tuple pattern makes error handling explicit and composable.
6

Generate OpenAPI documentation

Get automatic OpenAPI specs for your API:
// Add this to your server.ts
const openApiSpec = await api.getOpenApiSpec();

// Serve the spec
api.defineRoute({
  path: "/openapi.json",
  method: "GET",
  response: {
    200: z.any(),
  },
  handler: async (ctx) => {
    return ctx.json(200, openApiSpec);
  },
});
Access your OpenAPI spec at http://localhost:3000/openapi.json and use it with Swagger UI, Redoc, or any OpenAPI-compatible tool.

Next Steps

Now that you have a working API, explore more features:

API Reference

Explore all API framework features

Building APIs

Learn advanced API patterns

Background Jobs

Process tasks with Redis queues

Error Handling

Master result-based error patterns

Build docs developers (and LLMs) love