Kylndocs

Services & dependency injection

Anything that isn't view logic (data access, auth, third-party clients) belongs in a service. A service is a plain class Kyln can construct and hand to whatever needs it, so your routes and API endpoints stay focused on what they do, not on how their dependencies are built.

Defining a service

Extend Service and mark the class with a scope decorator. Here's a database service that opens a SQLite connection once and shares it:

import { Service, singleton } from "@kyln/core";
import { Database } from "bun:sqlite";

@singleton()
export class DatabaseService extends Service {
  readonly conn: Database;

  constructor(dbPath: string = "app.db") {
    super();
    this.conn = new Database(dbPath);
    this.conn.run("PRAGMA journal_mode = WAL");
  }
}

Using a service

Pull a service in with inject(). Assign it to a field and use it anywhere in the class:

import { Api, api, get, inject } from "@kyln/core";
import { DatabaseService } from "../services/db.service";

@api("/tasks")
export default class Tasks extends Api {
  private db = inject(DatabaseService);

  @get("/")
  async list() {
    const tasks = this.db.conn.query("SELECT id, title, done FROM tasks").all();
    return Response.json(tasks);
  }
}

You never call new DatabaseService(). inject() returns the instance Kyln manages, honoring its scope.

Services depend on services

A service can inject another service the same way. Because DatabaseService is a singleton, every service that injects it shares the one connection:

import { Service, singleton, inject } from "@kyln/core";
import { DatabaseService } from "./db.service";

@singleton()
export class AuthService extends Service {
  private db = inject(DatabaseService);

  login(email: string, password: string) {
    const row = this.db.conn
      .query("SELECT id, email, password FROM users WHERE email = ?")
      .get(email);
    // ...verify and return a session
  }
}

Scopes

A scope decorator sets a service's lifetime. The default is singleton.

Decorator Lifetime
@singleton() One instance for the whole app. The default. Use for connection pools, caches, shared clients.
@request() One instance per HTTP request. Use for per-request state such as the current user.
@transient() A fresh instance every time it's injected.

Request-scoped services resolve within a request context; Kyln establishes that context for you when handling a request.

Discovery and reachability

The pattern in this guide, import { SomeService } and then inject(SomeService), is all you need, and it's safe by construction. The import does double duty: it makes the service reachable, so the production build includes it, and it lets Kyln register the class the first time you inject it. Follow the examples here and a service is always available where you use it, in development and in production alike.

Two things are worth knowing so nothing surprises you:

  • A service is bundled only if something that ships imports it: a route, a layout, an API endpoint, or another reachable service, directly or transitively. A service that nothing imports isn't referenced by any shipped code, so it isn't in the build. Because injecting a service means importing it, following the pattern above keeps every service you actually use reachable.
  • Injecting a class auto-constructs it; injecting a token needs a registration. inject(DatabaseService) works with no explicit setup precisely because Kyln constructs a class on first use. Injection tokens are different: they have no class to construct, so an unregistered token throws No provider registered for: <name>, naming exactly what's missing. Either way the outcome is a clear one: a working instance or a loud error, never a silent undefined.

This only becomes a real consideration if you step off the documented path, for example resolving a service by a token() that was only picked up by dev-time discovery, without an import to anchor it in the build. As long as you import what you inject, reachability takes care of itself.

Next

Services back both your pages and your API endpoints. The API surface (@api, @get, @post, and request handling) is covered in API endpoints.