Context Extensions

Extend the request context with custom properties and services

Context extensions allow you to attach custom properties, services, and utilities to the request context, making them available in all route handlers.

Why Extend Context?

Common use cases include:

  • Database connections
  • Authentication state
  • Logger instances
  • Configuration
  • Utility functions

Basic Extension

Use the extend() method to add properties:

import { server } from "kitojs";
 
interface Extends {
  user: { id: string; name: string };
}
 
const app = server().extend<Extends>(ctx => {
  ctx.user = { id: "1", name: "Neo" };
});
 
app.get("/", ({ res, user }) => {
  res.json({ user });
});

Type-Safe Extensions

Extensions are fully type-safe:

interface Extensions {
  db: Database;
  config: AppConfig;
  logger: Logger;
}
 
const app = server()
  .extend<Extensions>(ctx => {
    ctx.db = db;
    ctx.config = config;
    ctx.logger = logger;
  });
 
app.get("/", ({ db, config, logger }) => { 
  // TypeScript knows about all extensions
  db.query("...");      // ✅ Type-safe
  config.apiKey;        // ✅ Type-safe
  logger.info("...");   // ✅ Type-safe
});

Return Style Extension

Return an object instead of mutating context:

const app = server().extend<{ db: Database }>(_ => {
  return {
    db: createDatabaseConnection()
  };
});

Multiple Extensions

Chain multiple extensions:

const app = server()
  .extend<{ db: Database }>(ctx => {
    ctx.db = db;
  })
  .extend<{ cache: Cache }>(ctx => {
    ctx.cache = cache;
  })
  .extend<{ logger: Logger }>(ctx => {
    ctx.logger = logger;
  });
 
app.get("/", ({ db, cache, logger }) => {
  // All extensions are available
  db.query("...");
  cache.get("...");
  logger.info("...");
});

Per-Request Extensions

Create request-specific data:

const app = server()
  .extend<{ requestId: string }>(ctx => {
    ctx.requestId = crypto.randomUUID();
  });
 
app.get("/", ({ requestId, logger, res }) => {
  logger.info(`Request ID: ${requestId}`);
  res.send("OK");
});

Service Container

Create a service container pattern:

interface Services {
  userService: {
    getById: (id: string) => Promise<User>;
    create: (data: UserData) => Promise<User>;
  };
  emailService: {
    send: (to: string, subject: string, body: string) => Promise<void>;
  };
}
 
const app = server().extend<Services>(ctx => ({
  userService: {
    getById: async (id) => ctx.db.query("SELECT * FROM users WHERE id = ?", [id]),
    create: async (data) => ctx.db.query("INSERT INTO users...", data)
  },
  emailService: {
    send: async (to, subject, body) => sendEmail(to, subject, body)
  }
}));
 
app.post("/users", async ctx => {
  const { userService, emailService, req, res } = ctx;
 
  const user = await userService.create(req.body);
  await emailService.send(user.email, "Welcome", "...");
  res.json(user);
});

Combining with Validation

Use extensions with validated data:

const userSchema = schema({
  body: t.object({
    name: t.str(),
    email: t.str().email()
  })
});
 
app.post("/users", async ({ db, req, res }) => {
  // Both validation and extensions work together
  const user = await db.query(
    "INSERT INTO users (name, email) VALUES (?, ?)",
    [req.body.name, req.body.email]
  );
 
  res.json(user);
}, userSchema);
💡 Tip

Extensions are created once per request, so they’re perfect for request-scoped services and state.

ℹ️ Note

Extensions run before route handlers but after middleware. Plan your architecture accordingly.