Pure Functional
Fossyl is designed around pure functions — no decorators, no base classes, no framework coupling. Every adapter in the ecosystem is just a function or an object implementing a simple interface.
Validators Are Just Functions
Section titled “Validators Are Just Functions”A validator in fossyl is a function that receives unknown and returns a typed value. That’s it.
// Validators are just pure functions: (data: unknown) => T// No framework coupling, no special decorators, no base classes
import { createRouter<BasePath extends string>(_: BasePath) => Router<BasePath> } from "@fossyl/core";
const routerRouter<"/api"> = createRouter<BasePath extends string>(_: BasePath) => Router<BasePath><"/api">("/api");
// Manual validator — no library neededconst createUserValidator(data: unknown) => { name: string; email: string } = (dataobject: unknown): { namestring: string; emailstring: string } => { if (typeof dataobject !== "object" || dataobject === null) { throw new Error("Request body must be an object"); } const objRecord<string, unknown> = dataobject as Record<string, unknown>; if (typeof objRecord<string, unknown>.namestring !== "string" || objRecord<string, unknown>.namestring.lengthnumber === 0) { throw new Error("name is required and must be a string"); } if (typeof objRecord<string, unknown>.emailstring !== "string" || !objRecord<string, unknown>.emailstring.includes(searchString: string, position?: number) => boolean("@")) { throw new Error("email must be a valid email address"); } return { name: objRecord<string, unknown>.namestring, email: objRecord<string, unknown>.emailstring };};
// Use the chain pattern — the return type becomes the body typeconst _createUserRouteRoute = {
path: string
method: RestMethod
steps: Steps[]
handler: Function
authenticator?: AuthenticationFunction<any>
validator?: ValidatorFunction<any>
queryValidator?: ValidatorFunction<any>
urlParamValidator?: ValidatorFunction<any>
paginationConfig?: PaginationConfig
hasTransaction: boolean
} = routerRouter<"/api"> .createEndpoint<Path extends `/api${string}`>(path: Path) => Endpoint<Path, true>("/api/users") .validator<RequestBody extends unknown>(
validatorFunction: ValidatorFunction<RequestBody>,
) => {
post: <Response extends ResponseData>(
handler: undefined extends RequestBody
? () => Promise<Response>
: (
body: RequestBody & { readonly __kind: "body" },
) => () => Promise<Response>,
) => Route
put: <Response extends ResponseData>(
handler: undefined extends RequestBody
? () => Promise<Response>
: (
body: RequestBody & { readonly __kind: "body" },
) => () => Promise<Response>,
) => Route
}(createUserValidator(data: unknown) => { name: string; email: string }) .post<Response extends ResponseData>(
handler: (
body: { name: string; email: string } & { readonly __kind: "body" },
) => () => Promise<Response>,
) => Route((body{ name: string; email: string } & { readonly __kind: "body" }) => async () => { // body is typed as { name: string; email: string } return { typeName: "User" as const, ...body{ name: string; email: string } & { readonly __kind: "body" }, id: crypto.randomUUID() }; });Use any validation library — Zod, Valibot, Yup, io-ts, or write your own. The type system flows from the return type of your validator.
Adapter Pattern
Section titled “Adapter Pattern”Fossyl defines several adapter interfaces. Each adapter type is an independent contract — they are standalone modules that get composed in your project’s entry point.
Database Adapter
Section titled “Database Adapter”Database adapters provide transaction management and client context. Created independently, then passed to the framework adapter:
// Database adapters implement the DatabaseAdapter interface.// Created independently, then composed into the framework adapter.import type"database" { DatabaseAdapter } from "@fossyl/core";import { expressAdapter(options?: ExpressAdapterOptions) => FrameworkAdapter<Application> } from "@fossyl/express";
const dbAdapterDatabaseAdapter: DatabaseAdapter = { type: "database", name: "kysely", client: db, defaultTransaction: true, autoMigrate: true,
onStartup() => Promise<void>: async () => { await migrateToLatest(); },
withTransaction<T>(fn: (ctx: DatabaseContext<unknown>) => Promise<T>) => Promise<any>: async (fn(ctx: DatabaseContext<unknown>) => Promise<T>) => { return db.transaction().execute((trx: any) => { return fn(ctx: DatabaseContext<unknown>) => Promise<T>({ client: trx, inTransaction: true }); }); },
withClient<T>(fn: (ctx: DatabaseContext<unknown>) => Promise<T>) => Promise<T>: async (fn(ctx: DatabaseContext<unknown>) => Promise<T>) => { return fn(ctx: DatabaseContext<unknown>) => Promise<T>({ client: db, inTransaction: false }); },};
const adapterFrameworkAdapter<Application> = expressAdapter(options?: ExpressAdapterOptions) => FrameworkAdapter<Application>({ cors: true, database: dbAdapterDatabaseAdapter,});
adapterFrameworkAdapter<Application>.register(routes: Route[]) => void(routes);await adapterFrameworkAdapter<Application>.listen(port: number) => Promise<void>(3000);Logger Adapter
Section titled “Logger Adapter”Logger adapters create per-request loggers. Any logging library works:
// Logger adapters are pure functional interfaces.// Created standalone, then passed to the framework adapter.import type"logger" { LoggerAdapter } from "@fossyl/core";import { expressAdapter(options?: ExpressAdapterOptions) => FrameworkAdapter<Application>, getLogger() => Logger } from "@fossyl/express";import { createRouter<BasePath extends string>(_: BasePath) => Router<BasePath> } from "@fossyl/core";
const routerRouter<"/api"> = createRouter<BasePath extends string>(_: BasePath) => Router<BasePath><"/api">("/api");
const pinoLoggerLoggerAdapter = {
type: "logger"
name: string
/** Create a logger instance for a specific request */
createLogger: (requestId: string) => Logger
}: LoggerAdapter = { type: "logger", name: "pino", createLogger(requestId: string) => {
info: (msg: string, meta: Record<string, unknown> | undefined) => any
warn: (msg: string, meta: Record<string, unknown> | undefined) => any
error: (msg: string, meta: Record<string, unknown> | undefined) => any
}: (requestIdstring) => ({ info(message: string, meta?: Record<string, unknown>) => void: (msgstring, metaRecord<string, unknown> | undefined) => pino.info(message: string, meta?: Record<string, unknown>) => void({ requestIdstring, ...metaRecord<string, unknown> | undefined }, msgstring), warn(msg: string, meta: Record<string, unknown> | undefined) => any: (msgstring, metaRecord<string, unknown> | undefined) => pino.warn(msg: string, meta: Record<string, unknown> | undefined) => any({ requestIdstring, ...metaRecord<string, unknown> | undefined }, msgstring), error(msg: string, meta: Record<string, unknown> | undefined) => any: (msgstring, metaRecord<string, unknown> | undefined) => pino.error(msg: string, meta: Record<string, unknown> | undefined) => any({ requestIdstring, ...metaRecord<string, unknown> | undefined }, msgstring), }),};
const _adapterFrameworkAdapter<Application> = expressAdapter(options?: ExpressAdapterOptions) => FrameworkAdapter<Application>({ cors: true, logger: pinoLoggerLoggerAdapter = {
type: "logger"
name: string
/** Create a logger instance for a specific request */
createLogger: (requestId: string) => Logger
},});
const _myRouteRoute = {
path: string
method: RestMethod
steps: Steps[]
handler: Function
authenticator?: AuthenticationFunction<any>
validator?: ValidatorFunction<any>
queryValidator?: ValidatorFunction<any>
urlParamValidator?: ValidatorFunction<any>
paginationConfig?: PaginationConfig
hasTransaction: boolean
} = routerRouter<"/api"> .createEndpoint<Path extends `/api${string}`>(path: Path) => Endpoint<Path, true>("/api/logs") .get<Response extends ResponseData>(
handler: () => Promise<Response>,
) => Route(async () => { const loggerLoggerAdapter = {
type: "logger"
name: string
/** Create a logger instance for a specific request */
createLogger: (requestId: string) => Logger
} = getLogger() => Logger(); loggerLoggerAdapter = {
type: "logger"
name: string
/** Create a logger instance for a specific request */
createLogger: (requestId: string) => Logger
}.info(message: string, meta?: Record<string, unknown>) => void("Handling request"); return { typeName: "Ok" as const, status: "logged" }; });Validation Adapter
Section titled “Validation Adapter”Validation adapters format errors for HTTP responses. The default is usually sufficient, but you can customize it:
// Validation adapters format errors for HTTP responses.// They are standalone modules — no framework coupling.import type"validation" { ValidationAdapter } from "@fossyl/core";
const _myValidationAdapterValidationAdapter = {
type: "validation"
name: string
/** Format validation errors for response */
formatError: (error: unknown) => {
message: string
details?: unknown
}
}: ValidationAdapter = { type: "validation", name: "my-validator",
formatError(
error: unknown,
) =>
| { message: string; details: any }
| { message: string; details?: undefined }: (error: unknown) => { if (error instanceof MyValidationError) { return { message: "Validation failed", details: error.fieldErrors, }; } return { message: "Validation failed" }; },};Adapter Composition
Section titled “Adapter Composition”Adapters are standalone modules composed in src/index.ts. Each is created independently and wired together through the framework adapter:
// Adapters are standalone modules composed in src/index.tsimport type"logger" { LoggerAdapter, DatabaseAdapter } from "@fossyl/core";import { expressAdapter(options?: ExpressAdapterOptions) => FrameworkAdapter<Application> } from "@fossyl/express";
const databaseDatabaseAdapter: DatabaseAdapter = { type: "database", name: "kysely", client: db, defaultTransaction: true, autoMigrate: true, onStartup() => Promise<void>: async () => {}, withTransaction<T>(fn: (ctx: DatabaseContext<unknown>) => Promise<T>) => Promise<T>: async (fn(ctx: DatabaseContext<unknown>) => Promise<T>) => fn(ctx: DatabaseContext<unknown>) => Promise<T>({ client: db, inTransaction: true }), withClient<T>(fn: (ctx: DatabaseContext<unknown>) => Promise<T>) => Promise<T>: async (fn(ctx: DatabaseContext<unknown>) => Promise<T>) => fn(ctx: DatabaseContext<unknown>) => Promise<T>({ client: db, inTransaction: false }),};
const loggerLoggerAdapter = {
type: "logger"
name: string
/** Create a logger instance for a specific request */
createLogger: (requestId: string) => Logger
}: LoggerAdapter = { type: "logger", name: "pino", createLogger(requestId: string) => {
info: (msg: string, meta: Record<string, unknown> | undefined) => any
warn: (msg: string, meta: Record<string, unknown> | undefined) => any
error: (msg: string, meta: Record<string, unknown> | undefined) => any
}: (requestIdstring) => ({ info(msg: string, meta: Record<string, unknown> | undefined) => any: (msgstring, metaRecord<string, unknown> | undefined) => pino.info(msg: string, meta: Record<string, unknown> | undefined) => any({ requestIdstring, ...metaRecord<string, unknown> | undefined }, msgstring), warn(msg: string, meta: Record<string, unknown> | undefined) => any: (msgstring, metaRecord<string, unknown> | undefined) => pino.warn(msg: string, meta: Record<string, unknown> | undefined) => any({ requestIdstring, ...metaRecord<string, unknown> | undefined }, msgstring), error(msg: string, meta: Record<string, unknown> | undefined) => any: (msgstring, metaRecord<string, unknown> | undefined) => pino.error(msg: string, meta: Record<string, unknown> | undefined) => any({ requestIdstring, ...metaRecord<string, unknown> | undefined }, msgstring), }),};
const adapterFrameworkAdapter<Application> = expressAdapter(options?: ExpressAdapterOptions) => FrameworkAdapter<Application>({ cors: true, databaseDatabaseAdapter, loggerLoggerAdapter = {
type: "logger"
name: string
/** Create a logger instance for a specific request */
createLogger: (requestId: string) => Logger
},});
adapterFrameworkAdapter<Application>.register(routes: Route[]) => void(routes);await adapterFrameworkAdapter<Application>.listen(port: number) => Promise<void>(3000);The framework adapter orchestrates them at runtime — it calls database.onStartup() on listen, wraps POST/PUT handlers in withTransaction(), and provides getLogger()/getDb() context accessors. But each adapter is its own standalone module.