Getting Started with Katal

This guide walks you from zero to a fully working Katal application with authentication, validation, and middleware.


Prerequisites

  • Bun ≥ 1.0 installed
  • Basic TypeScript familiarity

1. Create a Project

mkdir my-api && cd my-api
bun init -y
bun add katal

Add tsconfig.json:

{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "outDir": "./dist"
  }
}

2. Hello World

Create src/index.ts:

import { Application, Controller } from "katal";
import type { RequestContext } from "katal";

const app = new Application({ port: 3000 });
const router = app.getRouter();

class RootController extends Controller {
  async handle(_ctx: RequestContext) {
    return this.success({ message: "Hello from Katal!" });
  }
}

router.get("/", RootController);

await app.listen();
console.log("Running on http://localhost:3000");
bun run src/index.ts

3. Add a Route with Path Parameters

class UserController extends Controller {
  async handle(ctx: RequestContext) {
    const id = ctx.params.id;
    return this.success({ userId: id });
  }
}

router.get("/users/:id", UserController);

4. Parse a Request Body

class CreateUserController extends Controller {
  async handle(ctx: RequestContext) {
    const body = ctx.body as { name: string; email: string };
    // create user logic...
    return this.success({ created: body }, 201);
  }
}

router.post("/users", CreateUserController);

5. Add Input Validation

Katal provides schema-based validation directly on your controller.

import { Controller, Validator } from "katal";
import type { RequestContext } from "katal";

class CreateUserController extends Controller {
  protected schema = {
    name: { required: true, type: "string" as const, min: 2 },
    email: { required: true, type: "string" as const, pattern: /.+@.+\..+/ },
    age: { required: false, type: "number" as const, min: 13 },
  };

  async handle(ctx: RequestContext) {
    // ctx.body is type-safe once schema passes
    const { name, email } = ctx.body as { name: string; email: string };
    return this.success({ name, email }, 201);
  }
}

Validation errors return HTTP 422 automatically:

{
  "error": "Validation failed",
  "details": [
    { "field": "email", "message": "email is required" }
  ]
}

6. Add a Middleware

import type { Middleware } from "katal";

const logger: Middleware = async (ctx, next) => {
  const start = Date.now();
  const response = await next(ctx);
  console.log(`${ctx.request.method} ${ctx.url.pathname} — ${Date.now() - start}ms`);
  return response;
};

app.use(logger);

7. Add CORS

import { CorsMiddleware } from "katal";

app.middleware("cors", new CorsMiddleware({
  origins: ["https://my-frontend.com"],
  methods: ["GET", "POST"],
}));

app.use("cors");

8. Add JWT Authentication

import { Auth, createAuthMiddleware } from "katal";

const auth = new Auth({ secret: process.env.JWT_SECRET! });
app.middleware("auth", createAuthMiddleware(auth));

// Issue a token on login
class LoginController extends Controller {
  async handle(ctx: RequestContext) {
    const { email, password } = ctx.body as { email: string; password: string };
    // validate credentials...
    const token = auth.generateToken({ id: "1", email });
    return this.success({ token });
  }
}

// Protect a route
router.get("/profile", ProfileController, { middleware: ["auth"] });

9. Add Rate Limiting

import { RateLimitMiddleware } from "katal";

app.useRateLimiting({
  windowMs: 60_000,  // 1 minute
  max: 100,          // 100 requests per window
});

10. Organise with Route Groups

router.group("/api/v1", (r) => {
  r.group("/users", (r) => {
    r.get("/",          ListUsersController);
    r.get("/:id",       GetUserController);
    r.post("/",         CreateUserController, { middleware: ["auth"] });
    r.put("/:id",       UpdateUserController, { middleware: ["auth"] });
    r.delete("/:id",    DeleteUserController, { middleware: ["auth"] });
  });

  r.group("/posts", (r) => {
    r.get("/",          ListPostsController);
    r.post("/",         CreatePostController, { middleware: ["auth"] });
  });
});

11. Health Checks

app.addHealthCheck("database", async () => {
  // attempt a query
  return { status: "ok" };
});

app.enableHealthEndpoints(); // exposes /health, /health/ready, /health/live

12. Full Working Example

import {
  Application,
  Controller,
  Auth,
  createAuthMiddleware,
  CorsMiddleware,
  RateLimitMiddleware,
} from "katal";
import type { RequestContext } from "katal";

// --- App setup ---
const app = new Application({ port: 3000 });
const router = app.getRouter();
const auth = new Auth({ secret: process.env.JWT_SECRET ?? "dev-secret" });

// --- Middleware ---
app.middleware("cors", new CorsMiddleware({ origins: "*" }));
app.middleware("auth", createAuthMiddleware(auth));
app.use("cors");

app.useRateLimiting({ windowMs: 60_000, max: 200 });

// --- Health ---
app.enableHealthEndpoints();

// --- Controllers ---
class PingController extends Controller {
  async handle(_ctx: RequestContext) {
    return this.success({ pong: true });
  }
}

class LoginController extends Controller {
  protected schema = {
    email:    { required: true, type: "string" as const },
    password: { required: true, type: "string" as const },
  };

  async handle(ctx: RequestContext) {
    const { email } = ctx.body as { email: string; password: string };
    const token = auth.generateToken({ id: "1", email });
    return this.success({ token });
  }
}

class MeController extends Controller {
  async handle(ctx: RequestContext) {
    return this.success({ user: ctx.user });
  }
}

// --- Routes ---
router.get("/ping", PingController);
router.post("/login", LoginController);
router.get("/me", MeController, { middleware: ["auth"] });

// --- Start ---
await app.listen();

Next Steps

  • Read the Core guide for the full Application + Router API
  • Define reusable base controllers in HTTP
  • Set up a job queue: Queue
  • Write integration tests with the Testing Kit