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.