Kylndocs

API endpoints

API endpoints serve JSON. They live under src/api/, use the same dependency injection as the rest of your app, and are served under the /api prefix.

Defining an endpoint

An endpoint is a class that extends Api, decorated with @api(basePath). Each handler is a method decorated with an HTTP-method decorator:

import { Api, api, get, post, 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 ORDER BY created_at DESC")
      .all();
    return Response.json(tasks);
  }

  @post("/")
  async create(req: Request) {
    const { title } = await req.json();
    if (!title) {
      return Response.json({ error: "Title is required" }, { status: 400 });
    }
    this.db.conn.run("INSERT INTO tasks (title) VALUES (?)", [title]);
    return Response.json({ title, done: false }, { status: 201 });
  }
}

@api("/tasks") mounts this class under /api/tasks. The @get("/") handler answers GET /api/tasks; the @post("/") handler answers POST /api/tasks.

HTTP methods

Each HTTP verb has a decorator:

Decorator Method
@get(path) GET
@post(path) POST
@put(path) PUT
@patch(path) PATCH
@del(path) DELETE

The path is relative to the class's base path. Combine them for a full REST surface on one class.

The request and response

Handlers receive the standard Request and return a standard Response; Kyln doesn't wrap them. Read a JSON body with await req.json(), and build responses with Response.json(data, init):

@post("/")
async create(req: Request) {
  const body = await req.json();
  return Response.json({ received: body }, { status: 201 });
}

Route parameters

Declare a parameter in the decorator path with :name. Handlers receive the captured values as the second argument:

@post("/:id/toggle")
async toggle(req: Request, { params }: { params: Record<string, string> }) {
  this.db.conn.run("UPDATE tasks SET done = NOT done WHERE id = ?", [params.id]);
  return Response.json({ ok: true });
}

@del("/:id")
async remove(req: Request, { params }: { params: Record<string, string> }) {
  this.db.conn.run("DELETE FROM tasks WHERE id = ?", [params.id]);
  return Response.json({ ok: true });
}

POST /api/tasks/42/toggle binds params.id to "42".

Using services

API endpoints inject services exactly like the rest of your app. The example above pulls in DatabaseService; an authenticated endpoint would inject an auth service and check the request before proceeding:

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

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

  @get("/")
  async list(req: Request) {
    const user = this.auth.getAuthenticatedUser(req);
    if (!user) return Response.json({ error: "Unauthorized" }, { status: 401 });

    const tasks = this.db.conn
      .query("SELECT id, title, done FROM tasks WHERE user_id = ?")
      .all(user.id);
    return Response.json(tasks);
  }
}

Next

Configuration covers the settings that control the server, the router, and the build.