Katman
Best Practices

Testing & Mocking

Test Katman procedures without starting a server.

Three patterns for testing Katman APIs, from lightweight unit tests to full HTTP-level integration tests.

1. callable() — unit testing

Test a single procedure in isolation. The compiled pipeline runs (guards, wraps, validation), but there is no HTTP involved.

import {  } from "katman"
import { , ,  } from "vitest"

const  = k.query(
  z.object({ : z.number().optional() }),
  ({ ,  }) => .db.users.findMany({ : .limit }),
)

const  = (, {
  : () => ({
    : createTestDB(),
  }),
})

("listUsers", () => {
  ("returns users with limit", async () => {
    const  = await ({ : 2 })
    ().(2)
  })

  ("rejects invalid input", async () => {
    // @ts-expect-error — testing runtime validation
    await (({ : "bad" }))..()
  })
})

callable() is the fastest way to test. Use it when you want to verify a procedure's logic and validation without worrying about HTTP serialization.

Testing guarded procedures

Guards run as part of the compiled pipeline. Provide the context they need:

const  = k.mutation({
  : [auth],
  : z.object({ : z.string() }),
  : ({ ,  }) => .db.users.create(),
})

const  = callable(, {
  : () => ({
    : { : "Bearer test-token" },
    : createTestDB(),
  }),
})

it("creates a user when authenticated", async () => {
  const  = await ({ : "Alice" })
  expect(.name).toBe("Alice")
})

To test that a guard rejects correctly, omit the required context:

const  = callable(createUser, {
  : () => ({
    : {},
    : createTestDB(),
  }),
})

it("rejects unauthenticated requests", async () => {
  await expect(({ : "Alice" })).rejects.toThrow("UNAUTHORIZED")
})

2. createServerClient() — integration testing

Test the full router as a single unit. The server client compiles the router once and lets you call any procedure by path — same as the real client, but in-process.

import {  } from "katman/client/server"

const  = (appRouter, {
  : () => ({
    : createTestDB(),
    : { : "Bearer test-token" },
  }),
})

describe("user flow", () => {
  it("creates and lists users", async () => {
    await .users.create({ : "Alice" })
    await .users.create({ : "Bob" })

    const  = await .users.list({ : 10 })
    expect().toHaveLength(2)
    expect([0].name).toBe("Alice")
  })
})

This is the best pattern for integration tests. You exercise the full pipeline — guards, wraps, validation, routing — without starting a server or dealing with ports.

createServerClient() reuses the compiled handler for every call. It is fast enough to use in large test suites without worrying about setup overhead.

3. handler() + fetch() — HTTP-level testing

When you need to test the actual HTTP layer — status codes, headers, content negotiation, CORS — use handler() with the Fetch API:

const  = k.handler(appRouter)

describe("HTTP behavior", () => {
  it("returns 200 with JSON", async () => {
    const  = await (
      new ("http://localhost/users/list", {
        : "POST",
        : { "Content-Type": "application/json" },
        : .({ : 5 }),
      })
    )

    expect(.status).toBe(200)
    expect(.headers.get("content-type")).toContain("application/json")

    const  = await .json()
    expect().toHaveLength(5)
  })

  it("returns 400 for invalid input", async () => {
    const  = await (
      new ("http://localhost/users/list", {
        : "POST",
        : { "Content-Type": "application/json" },
        : .({ : "not-a-number" }),
      })
    )

    expect(.status).toBe(400)
  })

  it("negotiates MessagePack", async () => {
    const  = await (
      new ("http://localhost/users/list", {
        : "POST",
        : {
          "Content-Type": "application/json",
          "Accept": "application/x-msgpack",
        },
        : .({ : 5 }),
      })
    )

    expect(.headers.get("content-type")).toContain("application/x-msgpack")
  })
})

No server process, no port allocation. The handler() function takes a Request and returns a Response — the same Web API you use in production.

Which pattern to use

PatternBest forSpeedWhat it tests
callable()Unit testsFastestProcedure logic, validation, guards
createServerClient()Integration testsFastFull pipeline, routing, procedure interactions
handler() + fetch()HTTP testsMediumStatus codes, headers, content negotiation

Start with callable() for most tests. Move to createServerClient() when you need to test across procedures. Use handler() only when the HTTP layer matters.

What's next?

  • Servercallable() and handler() reference
  • ClientcreateServerClient() reference
  • Monorepo Setup — share types between packages for testing

On this page