The Function Signature
Three parameters. That's it.
Every Pikku function receives the same three arguments — no matter which protocol triggers it.
Services
Your toolbox — database, logger, JWT, email, anything you register. Destructure only what you need.
{ db, logger, jwt }Data
Typed, validated input — normalized from any protocol. Path params, body, query, message payload — all merged.
{ bookId, title }Wire
Session and optional protocol helpers. session works everywhere — protocol-specific fields like http or rpc only appear when relevant, and you never have to use them.
{ session, setSession }const getBook = pikkuFunc({
title: 'Get Book',
func: async (
{ db, logger }, // services — your toolbox
{ bookId }, // data — typed input
{ session } // wire — protocol context
) => {
logger.info(`Fetching book ${bookId}`)
const book = await db.getBook(bookId)
return { book, reader: session.userId }
}
})
Services
Your toolbox, injected
Services are dependency-injected into every function. Register once, destructure anywhere.
Singleton services
Created once at startup, shared across all requests. Database connections, loggers, third-party clients.
Wire services
Created fresh per request. Session loaders, audit contexts, per-request caches. Lazily instantiated only when destructured.
Destructure what you need
Only pull the services your function actually uses. Keeps code clean and makes dependencies explicit.
import { PikkuServiceMap } from '.pikku/pikku-types.gen.js'
// Singleton services — created once at startup
const singletonServices: PikkuServiceMap = {
db: new DatabaseClient(DATABASE_URL),
logger: createLogger({ level: 'info' }),
jwt: new JWTService(JWT_SECRET),
email: new EmailClient(SMTP_CONFIG),
}
// Wire services — created fresh per request
const wireServices = {
// Each request gets its own session loader
session: (services, wire) => loadSession(wire),
// Per-request audit context
audit: (services, wire) => new AuditLog(wire.requestId),
}
Contracts & Versioning
Never accidentally break a client
Pikku hashes every function's input and output schema. Change a contract without bumping the version and the build fails — before it reaches production.
Contracts are tracked automatically
Every function's name + input schema + output schema = a contract hash. The CLI stores these in a versions.json manifest.
Breaking changes fail the build
If you change a published schema without bumping the version, pikku versions-check fails. Add it to CI and breaking changes never ship by accident.
Version bumps are explicit
Set version: 2 on the function, run pikku versions-update, commit. Old and new versions coexist — no migration needed.
# CI catches breaking changes before deploy $ npx pikku versions-check ✗ getBook — contract changed without version bump Input schema hash: a1b2c3d4 → f9e8d7c6 Output schema hash: i9j0k1l2 → z5y4x3w2 Run: npx pikku versions-update after bumping to version 2
// v1 — kept around for old clients
const getBookV1 = pikkuFunc({
title: 'Get Book',
version: 1,
input: z.object({ bookId: z.string() }),
output: z.object({ title: z.string() }),
func: async ({ db }, { bookId }) => {
return await db.getBook(bookId)
}
})
// v2 — the latest version, called by default
const getBook = pikkuFunc({
title: 'Get Book',
version: 2,
input: z.object({ bookId: z.string(), format: z.enum(['full', 'summary']) }),
output: z.object({ title: z.string(), author: z.string(), isbn: z.string() }),
func: async ({ db }, { bookId, format }) => {
return await db.getBook(bookId, format)
}
})
Session & Auth
One session API, every transport
Whether the request arrives over HTTP, WebSocket, or CLI — your function reads and writes the session the same way.
Middleware loads
Session populated from cookie, token, or connection state
Function receives
Access session via the wire parameter — read userId, role, etc.
Function modifies
Call setSession() or clearSession() to update
Middleware persists
Changes saved back to the transport — cookie, store, etc.
const login = pikkuFunc({
title: 'Login',
func: async (
{ jwt, db },
{ email, password },
{ setSession }
) => {
const user = await db.verifyCredentials(email, password)
const token = jwt.sign({ userId: user.id, role: user.role })
// Works the same whether it's an HTTP cookie,
// WebSocket connection, or CLI token
setSession({ userId: user.id, role: user.role })
return { token }
}
})
const getMe = pikkuFunc({
title: 'Get Current User',
func: async ({ db }, {}, { session }) => {
// session is loaded by middleware before
// your function runs — same API everywhere
return await db.getUser(session.userId)
},
permissions: { user: isAuthenticated }
})
Write Once, Wire Everywhere
One function. Every protocol.
The same function handles HTTP requests, WebSocket messages, queue jobs, CLI commands, and MCP tools — zero duplication.
Same Function
getBook(services, data, wire)// Define once
const getBook = pikkuFunc({
title: 'Get Book',
func: async ({ db }, { bookId }, { session }) => {
return await db.getBook(bookId)
},
permissions: { user: isAuthenticated }
})
// Wire to everything
wireHTTP({ method: 'get', route: '/books/:bookId', func: getBook })
wireWebSocket({ channel: 'books', func: getBook })
wireQueue({ queue: 'book-requests', func: getBook })
wireCLI({ command: 'get-book', func: getBook })
wireMCP({ tool: 'get_book', func: getBook })
Zero duplication
Business logic lives in one place. Each wire adapts the protocol to your function.
Same permissions
Auth and permission checks apply regardless of which wire triggers the function.
Same types
Input and output types are shared. Change once, every wire gets the update.
Start building in 5 minutes
One command to scaffold a project. Your first function will work across every protocol from day one.
MIT Licensed · Works with Express, Fastify, Lambda & Cloudflare