Skip to content
fossyl

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.

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 needed
const 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 type
const _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.

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 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 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 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" };
},
};

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.ts
import 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.