Skip to main content
Pikku Fabric use case

CLI + API from
the same function.

Write your business logic once. Get a REST API for your app, a CLI for your team, and an RPC client for internal services — all with the same auth and validation.

Your team needs a CLI to manage users. Your app needs an API for the same operations. Your internal service needs an RPC client. So you write the logic three times — once per entry point. Then someone fixes a bug in the CLI and forgets the API.

One function. Three entry points.

functions + wiring
// The function — written once
const resetPassword = pikkuFunc({
input: z.object({ userId: z.string(), notify: z.boolean().default(true) }),
output: z.object({ success: z.boolean() }),
func: async ({ db, email }, { userId, notify }) => {
const token = await db.resetTokens.create({ userId })
if (notify) await email.send({ to: userId, template: 'reset', token })
return { success: true }
},
permissions: { admin: isAdmin },
})

// Wire to API (for the app)
wireHTTP({ method: 'post', route: '/admin/reset-password', func: resetPassword })

// Wire to CLI (for the team)
wireCLI({ program: 'admin', commands: {
'reset-password': pikkuCLICommand({
parameters: '<userId>',
options: { notify: { flag: '--no-notify', default: true } },
func: resetPassword,
}),
}})

// Wire to RPC (for internal services)
wireRPC({ func: resetPassword })
all three work
# API
curl -X POST /admin/reset-password -d '{"userId":"usr_1"}'
# CLI
pikku admin reset-password usr_1 --no-notify
# RPC (from another service)
await client.resetPassword({ userId: 'usr_1' })
Same function. Same auth check. Same result.
Auto-generated CLI

Help text, arg parsing, autocomplete.

REST API

Typed endpoints + OpenAPI docs.

RPC client

Type-safe, zero-config.

Generated types

One source of truth.

Stop writing the same logic three times.

One function. API, CLI, and RPC. Deploy.