Skip to main content
Core Concept

Your toolbox.
Injected.

Two factory functions — pikkuServices and pikkuWireServices — wire your entire application together with full type safety and tree-shaking.

// Every function destructures
// only what it needs
async (
{ db, logger, jwt }
...
) => { ... }

// Pikku tracks which services
// each function uses → tree-shaking
The Two Factories

Two functions. Everything wired.

pikkuServices creates singletons at startup. pikkuWireServices creates per-request services. That's the whole model.

pikkuServices — Singletons

Created once at startup, shared across all requests. Database pools, loggers, JWT, third-party clients. Receives (config, existingServices).

pikkuWireServices — Per-request

Created fresh per HTTP request, queue job, CLI command, or WebSocket lifetime. Sessions, transactions, audit contexts. Receives (singletonServices, wire).

Destructure what you need

Functions declare dependencies explicitly. Pikku merges singleton + wire services so your function sees one flat object. Only pull what you use.

services.tsstartup
export const createSingletonServices = pikkuServices(
async (config, existingServices) => {
const variables =
existingServices?.variables ??
new TypedVariablesService(new LocalVariablesService());
const secrets =
existingServices?.secrets ??
new TypedSecretService(new LocalSecretService(variables));
const logger =
existingServices?.logger ?? new JsonConsoleLogger();
const schema =
existingServices?.schema ?? new CFWorkerSchemaService(logger);
const paymentService =
existingServices?.paymentService ?? new FakePaymentService();

// In CF Workers DATABASE_URL is injected by Fabric.
// In local dev (pikku dev) existingServices.kysely is the node:sqlite instance.
// In tests existingServices.kysely is provided by the test support layer.
let kysely: Kysely<DB> | undefined;
if (existingServices?.kysely) {
kysely = existingServices.kysely as Kysely<DB>;
} else {
const databaseUrl = await variables.get('DATABASE_URL');
if (databaseUrl) {
if (/^postgres(ql)?:\/\//.test(databaseUrl)) {
const [{ PostgresJSDialect }, postgres] =
await Promise.all([
import('kysely-postgres-js'),
import('postgres'),
]);
kysely = new Kysely<DB>({
dialect: new PostgresJSDialect({
postgres: postgres.default(databaseUrl),
}),
plugins: [new CamelCasePlugin()],
});
} else {
kysely = new Kysely<DB>({
dialect: new LibsqlWebDialect({ url: databaseUrl }),
plugins: [new CamelCasePlugin()],
});
}
} else {
throw new Error(
'kysely not provided: set DATABASE_URL or pass kysely via existingServices',
);
}
}

return {
...(existingServices ?? {}),
config,
variables,
secrets,
logger,
schema,
kysely,
paymentService,
audit: existingServices?.audit ?? new NoopAuditService(),
};
},
);
services.tsper-request
import { pikkuWireServices } from '../.pikku/pikku-types.gen.js';

export const createWireServices = pikkuWireServices(
async (_singletonServices, _wire) => {
// Created fresh per HTTP request, queue job, CLI command, or WebSocket lifetime.
// Pikku merges these with singletons so functions see one flat object.
// Add per-request services here — e.g. scoped loggers, transactions, etc.
return {};
},
);
Tree-Shaking

Only load what you actually use.

Pikku's CLI generates requiredSingletonServices — a map of which services your filtered functions need. Pair it with dynamic import() calls to skip everything else.

CLI scans destructuring

Pikku analyzes which services each function, middleware, and permission actually destructures from the services parameter.

Generates requiredSingletonServices

A boolean map marking each service true or false. Plus a RequiredSingletonServices type that narrows your factory's return type.

Dynamic imports skip unused services

Guard heavy imports with if (requiredSingletonServices.x). When a service isn't needed, the import is never executed — zero cold-start overhead.

.pikku/pikku-services.gen.tsauto-generated
// .pikku/pikku-services.gen.ts  (auto-generated)
export const requiredSingletonServices = {
'kysely': true, // used by getItem, createOrder, etc.
'paymentService': true, // used by processPayment
'logger': true, // used everywhere
'secrets': false, // not used by any wired function
} as const

export type RequiredSingletonServices =
Pick<SingletonServices, 'kysely' | 'paymentService' | 'logger'>
& Partial<Omit<SingletonServices, 'kysely' | 'paymentService' | 'logger'>>
services.tsyour code
import { requiredSingletonServices } from '../.pikku/pikku-services.gen.js';

// requiredSingletonServices is a map of booleans generated by pikku.
// If a service is false, no wired function needs it — safe to skip.
if (requiredSingletonServices.kysely) {
// Dynamic import only when actually needed — skip on cold starts that don't use DB
const { Kysely, CamelCasePlugin } = await import('kysely');
(void Kysely, CamelCasePlugin); // referenced to satisfy linter in snippet context
}
Faster cold starts
Health-check endpoints don't load your database driver. Payment endpoints don't load your email SDK.
Same codebase
Deploy as monolith, microservices, or individual functions — filter with CLI flags, no code changes.
Type-safe
RequiredSingletonServices narrows your factory return type so TypeScript catches missing services at compile time.

Start building in 5 minutes.

Scaffold a project with services pre-configured. Add your own, destructure what you need, and deploy anywhere.

$ npm create pikku@latest

MIT Licensed · Works with Express, Fastify, Lambda & Cloudflare