Skip to content

Routes

Routing is how you map URLs to handler functions. Kyrin uses a radix tree internally, which means route matching is fast — O(k) where k is the path length.

Basic Routes

Register routes using HTTP method shortcuts:

typescript
app.get("/users", handler);
app.post("/users", handler);
app.put("/users/:id", handler);
app.patch("/users/:id", handler);
app.delete("/users/:id", handler);
app.options("/users", handler);
app.head("/users", handler);

Or use the generic on() method:

typescript
app.on("GET", "/users", handler);
app.on("POST", "/users", handler);

Catch-All Method

Register a handler for all HTTP methods on a path:

typescript
app.all("/health", () => ({ status: "ok" }));

This registers the same handler for GET, POST, PUT, DELETE, PATCH, OPTIONS, and HEAD.

Path Parameters

Use :name syntax to capture dynamic segments:

typescript
app.get("/users/:id", (c) => {
  const userId = c.param("id");
  return { userId };
});

app.get("/posts/:postId/comments/:commentId", (c) => {
  const postId = c.param("postId");
  const commentId = c.param("commentId");
  return { postId, commentId };
});

Parameters are always strings. Parse them as needed:

typescript
app.get("/users/:id", (c) => {
  const id = parseInt(c.param("id")!, 10);
  if (isNaN(id)) {
    return c.json({ error: "Invalid ID" }, 400);
  }
  // ...
});

Query Parameters

Access query string values using c.query():

typescript
// GET /search?q=hello&page=2
app.get("/search", (c) => {
  const query = c.query("q"); // "hello"
  const page = c.query("page"); // "2"
  return { query, page };
});

Query parameters are also strings (or null if not present).

Route Groups (Mounting Routers)

For larger apps, split routes into separate routers and mount them with a prefix:

Basic user.ts Routes File:

typescript
// src/routes/user.ts
import { Router } from "kyrin"

export const userRouter = new Router()

userRouter.get('/',()=>{
	return {"message":"Hello User Kyrin"}
})

userRouter.get('/:id',(c)=>{
	return {"message":"User Found"}
})

userRouter.post('/create',(c)=>{
	return {"message":"User Created"}
})

Main File index.ts:

typescript
// src/index.ts
import { Kyrin } from "kyrin"
import { userRouter } from "./routes/user";

const app = new Kyrin()

// Mouted them
app.route("/users", userRouter); // /users, /users/:id

app.listen(3000);

This keeps your code organized and makes route handlers reusable.

Guarded Routes

Use guard() to wrap a group of routes with a middleware:

typescript
const auth = async (c:any, next:any) => {
  const token = c.header("Authorization");
  if (!token) {
    return c.json({ error: "Unauthorized" }, 401);
  }
  // Validate token, set user...
  c.store.user = { id: 1, name: "John" };
  await next();
};

app.guard(auth, (app) => {
  app.get("/profile", (c:any) => c.store.user);
  app.get("/settings", (c:any) => ({ user: c.store.user }));
});

Routes inside guard() will only execute if the middleware calls next().

Route Matching Order

Routes are matched in the order they were registered. More specific routes should be registered before generic ones:

typescript
// Good — specific first
app.get("/users/me", () => "Current user");
app.get("/users/:id", (c) => `User ${c.param("id")}`);

// Bad — generic catches "me" as an ID
app.get("/users/:id", (c) => `User ${c.param("id")}`);
app.get("/users/me", () => "Current user"); // Never reached

Static Routes vs Dynamic Routes

Kyrin optimizes static routes (paths without parameters) with O(1) lookup:

typescript
// O(1) lookup — cached in a Map
app.get("/", handler);
app.get("/about", handler);
app.get("/contact", handler);

// O(k) lookup — uses radix tree
app.get("/users/:id", handler);
app.get("/posts/:id/comments", handler);

You don't need to do anything special — the framework handles this automatically.

Trailing Slashes

Kyrin treats /users and /users/ as different paths. If you want to handle both:

typescript
app.get("/users", handler);
app.get("/users/", handler);

// Or use a redirect
app.get("/users/", (c) => c.redirect("/users", 301));

404 Handling

When no route matches, Kyrin returns a plain "Not Found" response with status 404. To customize this, add a catch-all route at the end:

typescript
// Register all your routes first...
app.get("/", handler);
app.get("/about", handler);

// Then add a fallback
app.all("/*", (c) => {
  return c.json({ error: "Not found", path: c.path }, 404);
});

Note: Wildcard (*) matching is supported for catch-all patterns.

Complete Example

typescript
import { Kyrin, Router } from "kyrin";

// API routes
const api = new Router();
api.get("/health", () => ({ status: "ok" }));
api.get("/users", () => [{ id: 1, name: "Alice" }]);
api.get("/users/:id", (c) => ({ id: c.param("id") }));
api.post("/users", async (c) => {
  const body = await c.body<{ name: string }>();
  return { created: body.name };
});

// App
const app = new Kyrin();

// Mount API
app.route("/api/v1", api);

// Static pages
app.get("/", () => "Welcome!");
app.get("/about", () => "About us");

// 404 fallback
app.all("/*", (c) => c.json({ error: "Not found" }, 404));

app.listen(3000);

Released under the MIT License.