Type-safe building blocks for real backends.

APIs, queues, workflows, ORM, and more - with zero runtime dependencies. Import only what you use.

Read the docs
semola/api/hello.ts
import { Api } from "semola/api";import { z } from "zod";const api = new Api();api.defineRoute({  path: "/hello/:name",  method: "GET",  request: {    params: z.object({ name: z.string() }),  },  response: {    200: z.object({ message: z.string() }),  },  handler: async (c) => {    return c.json(200, {      message: `Hello, ${c.req.params.name}!`,    });  },});api.serve(3000);

Showing API example from semola/api

Why teams reach for it

Small surface, clear tradeoffs, no surprise lock-in.

01

Type-safe by default

Inputs, outputs, and errors stay typed end to end. Catch mistakes at compile time instead of in production logs.

02

Easy to pick up

Small APIs, clear names, short examples. Read a snippet, paste it in, keep shipping.

03

One toolkit, not a pile

APIs, jobs, data, auth, and utilities in one package. Skip the ritual of wiring half a dozen libs for every new app.

In practice

Same shape every time: pick a module, read a short pitch, steal the snippet.

01semola/api

A typed HTTP API framework

Define REST routes with request and response schemas. Works with Zod, Valibot, ArkType, or any Standard Schema library.

Docs →
hello.ts
import { Api } from "semola/api";import { z } from "zod";const api = new Api();api.defineRoute({  path: "/hello/:name",  method: "GET",  request: {    params: z.object({ name: z.string() }),  },  response: {    200: z.object({ message: z.string() }),  },  handler: async (c) => {    return c.json(200, {      message: `Hello, ${c.req.params.name}!`,    });  },});api.serve(3000);
02semola/cron

Cron jobs that run in-process

Schedule handlers with a cron expression or alias. Start, stop, and ask for the next fire time from one object.

Docs →
jobs.ts
import { Cron } from "semola/cron";const daily = new Cron({  name: "daily-report",  schedule: "@daily",  handler: async () => {    await sendReport();  },});daily.run();
03semola/orm

A typed ORM for Bun

Define tables once. findFirst, inserts, relations, and transactions infer from the schema - SQLite or Postgres via Bun SQL.

Docs →
users.ts
import {  createOrm,  defineTable,  string,  uuid,} from "semola/orm";const users = defineTable({  sqlName: "users",  columns: {    id: uuid("id").primaryKey().notNull(),    email: string("email").unique().notNull(),  },});const db = createOrm({  adapter: "sqlite",  url: ":memory:",  tables: { users },});const user = await db.users.findFirst({  where: { email: "hi@semola.dev" },});
04semola/policy

ABAC authorization as rules

Attribute-based access control with allow and forbid rules over your domain types. Forbid always wins.

Docs →
posts.ts
import { Policy, eq } from "semola/policy";type Post = {  id: number;  authorId: number;  status: "draft" | "published";};const posts = new Policy<Post>();posts.allow({  action: "read",  conditions: { status: eq("published") },});posts.allow({  action: ["update", "delete"],  conditions: { authorId: eq(user.id) },});const canEdit = posts.can("update", post);
05semola/queue

Redis queues with retries

Background job queues on Redis with concurrency, timeouts, and exponential backoff. Enqueue work without reinventing the worker loop.

Docs →
jobs.ts
import { Queue } from "semola/queue";const emails = new Queue({  name: "emails",  redis: redisClient,  concurrency: 4,  retries: 3,  handler: async (data) => {    await sendEmail(data.to, data.subject);  },});await emails.enqueue({  to: "user@example.com",  subject: "Welcome",});
06semola/workflow

Durable workflows that resume

Multi-step workflows with named steps that persist outputs. Resume after a crash and skip work that already completed.

Docs →
order.ts
import { defineWorkflow } from "semola/workflow";const fulfillOrder = defineWorkflow<{ orderId: string }>({  name: "fulfill-order",  redis: redisClient,  handler: async ({ input, step }) => {    const payment = await step("charge", async () => {      return charge(input.orderId);    });    await step("ship", async () => {      await ship(payment.orderId);    });  },});await fulfillOrder.start({ orderId: "ord_123" });

Ready when you are

Install the package, open the docs, ship the first typed route.

Getting started