Authoring Plugins
Create custom service and resource plugins with scoped routes and typed APIs.
Plugins package modular capabilities into reusable TypeScript descriptors that install onto services or resources.
Authoring a Service Plugin (defineServicePlugin)
A service plugin registers actions and raw HTTP endpoints onto a host service using setup({ service, routes }):
import { defineServicePlugin, t, type PluginRoutes } from "@brickkit/core";
export interface GreetingOptions {
salutation?: string;
routes?: PluginRoutes<"app" | "service">;
}
export function greeting(options: GreetingOptions = {}) {
const salutation = options.salutation ?? "Hello";
return defineServicePlugin({
name: "example/greeting",
routes: options.routes,
setup({ service, routes }) {
// 1. Register an action onto the service mesh
const hello = routes.action({
name: "hello",
method: "GET",
path: `/${service.name}/hello`,
input: t.Object({
name: t.String({ minLength: 1 }),
}),
output: t.String(),
execute: ({ input }) => `${salutation}, ${input.name}!`,
});
// 2. Register a raw HTML endpoint (outside the JSON action mesh)
routes.endpoint({
name: "welcome",
method: "GET",
path: `/${service.name}/welcome.html`,
handler: async ({ set }) => {
set.headers["content-type"] = "text/html; charset=utf-8";
return `<!DOCTYPE html><html><body><h1>${salutation}!</h1></body></html>`;
},
});
// Return a typed API namespace for callers
return {
hello,
salutation,
};
},
});
}Installing the Service Plugin
Install onto a service using service.use():
import { defineService } from "@brickkit/core";
import { greeting } from "../plugins/greeting";
export const greeterService = defineService("greeter");
// Install the plugin
export const api = greeterService.use(greeting({ salutation: "Welcome" }));
// In-process TypeScript calls are fully typed:
const message = await api.hello({ input: { name: "Ada" } });
console.log(message); // "Welcome, Ada!"Authoring a Resource Plugin (defineResourcePlugin)
A resource plugin attaches behavior to a specific database table and entity:
import { defineResourcePlugin, eq } from "@brickkit/core";
export function slugLookup(options: { slugColumn: any }) {
return defineResourcePlugin({
name: "example/slugLookup",
setup({ resource, service, routes }) {
// Return helper methods attached directly onto the resource object
return {
findBySlug: async (slug: string, ctx: any) => {
if (!ctx.db) throw new Error("Database required");
const rows = await ctx.db
.select()
.from(resource.table)
.where(eq(options.slugColumn, slug))
.limit(1);
return rows[0] ?? null;
},
};
},
});
}Installing the Resource Plugin
Install onto a resource using resource.use():
const article = service
.resource({ name: "article", table: articlesTable, id: articlesTable.id })
.use(slugLookup({ slugColumn: articlesTable.slug }));
// The returned resource is augmented with findBySlug:
const found = await article.findBySlug("my-post-slug", ctx);Plugin Design Principles
- Synchronous Setup:
setup()must execute synchronously. Asynchronous connection initialization or network lookups are prohibited during registration. - Connection Flow: Use
ctx.dbprovided during request handling. Never create persistent connection pools inside plugin constructors. - Collision Avoidance: Resource plugin contributions must not overwrite existing resource properties or prototype methods.
- Isolated Descriptors: Reinstalling a plugin with a different descriptor of the same name fails. Reuse descriptors when sharing instances.
Schema
The resource-plugin example expects an application-owned articles table with an identity column and a slug column. Declare the table before registering the resource. A unique slug makes findBySlug unambiguous.
Table Name: articles
| Field | Type | Description |
|---|---|---|
id | text | Article identifier. PK |
slug | text | Slug used by findBySlug. Unique |