Basic Usage
Your first flows, typed client, and tests — using the Notes example.
This page is a short tour of the shapes you will use every day. All snippets are from the Notes teaching app — the same source the README and doc-drift gate check.
Scaffold Notes
bunx create-oke@latest my-notes --from-example notes
cd my-notes
oke devApp on :6530, Console on :6533. Keep both open while you read.
Mental model (30 seconds)
- Trigger — how work starts (
http.post,every("10m"), a signal, …) - Contracts —
in/out/errorsschemas do— the body; it talks to the world only throughfx- Effects — inferred (reads, writes, emits, …) — you do not annotate them by hand
Flows
A flow is a trigger, contracts, and a do:
examples/notes/src/flows/notes/index.ts
import { on, flow, http } from "okengine";
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
import { z } from "zod";
import { db } from "../../core";
import { notes } from "../../schema";
// Contracts derived from the schema — one source of truth, refined where the API is stricter
const NewNote = createInsertSchema(notes, { title: (s) => s.min(1).max(120) })
.omit({ id: true, createdAt: true });
const Note = createSelectSchema(notes);
const NoteId = z.object({ id: z.string() });
const NotFound = z.object({});
export const create = on(http.post("/notes"), flow({
in: NewNote,
out: NoteId,
do: async (input, fx) => {
const [note] = await fx.store(db).insert(notes).values(input).returning();
return { id: note.id };
},
}));
// effects → writes[sql:notes]
export const list = on(http.get("/notes"), flow({
out: Note.array(),
do: (_, fx) => fx.store(db).select().from(notes),
}));
// effects → reads[sql:notes]
export const get = on(http.get("/notes/:id"), flow({
in: NoteId,
out: Note,
errors: { NotFound },
do: async ({ id }, fx) => (await fx.store(db).findById(notes, id)) ?? fx.fail("NotFound", {}),
}));
export const remove = on(http.delete("/notes/:id"), flow({
in: NoteId,
errors: { NotFound },
do: async ({ id }, fx) => {
const deleted = await fx.store(db).delete(notes, id);
if (!deleted) return fx.fail("NotFound", {});
},
}));Two rules to remember
fxis the only door to the outside world.- Errors are values (
fx.fail), not thrown exceptions.
What to notice in create:
in/outare Zod schemas — the typed client and OpenAPI come from the same contractsfx.store(db)is a Store effect — the comment under the flow shows what the compiler infers- No separate “repository layer” ceremony; the flow is the behavior
Wire the app
Adopt flow modules into an oke app. The namespace key becomes the client namespace; each export becomes a method.
examples/notes/src/app.ts
import { oke } from "okengine";
import * as notes from "./flows/notes";
export const app = oke({ name: "notes" }).adopt({ notes });
export type App = typeof app; // ← the client needs nothing elseTyped client
No separate codegen project to keep in sync — import the app type (and optionally $routes):
import { createClient } from "okengine/client";
import type { App } from "../src/app";
import { app } from "../src/app";
const api = createClient<App>("http://localhost:6530", { $routes: app.$routes });
// equivalently: const api = createClient(app, "http://localhost:6530");
const { data, error } = await api.notes.get({ id: "n_1" });
if (error?.code === "NotFound") show("gone");
else console.log(data.title); // ← typed, no codegen ✅data and error are narrowed from your flow contracts. NotFound is the same code you returned with fx.fail.
Tests
Swap the world for memory drivers with createTestApp — same client API as production:
examples/notes/tests/notes.test.ts
import { test, expect } from "bun:test";
import { createTestApp } from "okengine/test";
import { app } from "../src/app";
test("create then read", async () => {
const t = await createTestApp(app); // memory driver, automatic
const { data } = await t.api.notes.create({ title: "First", body: "Hello" });
const { data: note } = await t.api.notes.get({ id: data!.id });
expect(note!.title).toBe("First");
});bun testChecklist
After this page you should be able to:
- Explain
on(Trigger) → Effectsin one sentence - Write a flow with
in/out/doand usefx.store - Call that flow from
createClient - Cover it with
createTestApp+bun test