.claude/skills/pikku-services/SKILL.md
Use when setting up dependency injection, creating custom services, or configuring the service layer in a Pikku app. Covers pikkuServices (singleton), pikkuWireServices (per-request), service typing, built-in services, and tree-shaking. TRIGGER when: code uses pikkuServices/pikkuWireServices, user asks about services.ts, dependency injection, service factories, or built-in services (ConsoleLogger, JoseJWTService). DO NOT TRIGGER when: user asks about auth middleware (use pikku-security) or secrets/variables (use pikku-config).
npx skillsauth add pikkujs/pikku pikku-servicesInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Pikku uses factory functions for dependency injection. Singleton services are created once at startup. Wire services are created fresh per request/job/command.
pikku info functions --verbose # See which services existing functions use
pikku info tags --verbose # Understand project organization
See pikku-concepts for the core mental model.
pikkuServices(factory)Create singleton services — instantiated once at server startup.
import { pikkuServices } from '#pikku'
const createSingletonServices = pikkuServices(
async (config, existingServices?) => {
// config: your CoreConfig object
// existingServices: optional, for chaining factories
return {
config,
logger: Logger,
jwt: JWTService,
database: DatabasePool,
// ...any custom services
}
}
)
pikkuWireServices(factory)Create per-request services — fresh instance for each HTTP request, queue job, CLI command, etc.
import { pikkuWireServices } from '#pikku'
const createWireServices = pikkuWireServices(
async (singletonServices, wire) => {
// singletonServices: all singleton services
// wire: transport context (session, channel, etc.)
// Pikku merges these with singleton services automatically
return {
userSession: UserSessionService,
dbTransaction: DatabaseTransaction,
}
}
)
After npx pikku prebuild, Pikku generates a manifest of which services are actually used:
// .pikku/pikku-services.gen.ts (auto-generated)
export const requiredSingletonServices = {
database: true, // used by getUser, deleteUser
audit: true, // used by deleteUser
cache: false, // not used by any wired function
jwt: true, // used by auth middleware
} as const
export type RequiredSingletonServices = Pick<
SingletonServices,
'database' | 'audit' | 'jwt'
> &
Partial<Omit<SingletonServices, 'database' | 'audit' | 'jwt'>>
const createSingletonServices = pikkuServices(
async (config, existingServices) => {
const logger = new ConsoleLogger()
const database = new DatabasePool(config.database)
await database.connect()
const jwt = new JoseJWTService(
async () => [{ id: 'my-key', value: JWT_SECRET }],
logger
)
return {
config,
logger,
database,
jwt,
books: new BookService(),
}
}
)
const createWireServices = pikkuWireServices(
async (singletonServices, wire) => {
return {
userSession: createUserSessionService(wire),
dbTransaction: new DatabaseTransaction(singletonServices.database),
}
}
)
Functions destructure services from the first parameter:
const getUser = pikkuFunc({
title: 'Get User',
func: async ({ db, logger, jwt }, { userId }) => {
logger.info('Fetching user', { userId })
const user = await db.getUser(userId)
return { user }
},
})
Use the generated manifest to conditionally import heavy dependencies:
import { requiredSingletonServices } from '.pikku/pikku-services.gen.js'
const createSingletonServices = pikkuServices(async (config) => {
const logger = new ConsoleLogger()
let jwt: JWTService | undefined
if (requiredSingletonServices.jwt) {
const { JoseJWTService } = await import('@pikku/jose')
jwt = new JoseJWTService(keys, logger)
}
let database: Database | undefined
if (requiredSingletonServices.database) {
database = await createDatabase(config.databaseUrl)
}
return { config, logger, jwt, database }
})
| Service | Package | Purpose |
| ----------------------- | -------------------- | --------------------------- |
| ConsoleLogger | @pikku/core/services | Console-based logging |
| JoseJWTService | @pikku/jose | JWT sign/verify via jose |
| LocalSecretService | @pikku/core/services | Local development secrets |
| LocalVariablesService | @pikku/core/services | Local environment variables |
| PinoLogger | @pikku/pino | Structured logging via Pino |
// services.ts
import { pikkuServices, pikkuWireServices } from '#pikku'
import { ConsoleLogger } from '@pikku/core/services'
import { JoseJWTService } from '@pikku/jose'
// Custom service
class TodoStore {
private todos: Map<string, Todo> = new Map()
async create(title: string, priority: string) {
const todo = { id: crypto.randomUUID(), title, priority, completed: false }
this.todos.set(todo.id, todo)
return todo
}
async get(id: string) {
return this.todos.get(id)
}
async list() {
return [...this.todos.values()]
}
async delete(id: string) {
this.todos.delete(id)
}
}
export const createSingletonServices = pikkuServices(async (config) => {
const logger = new ConsoleLogger()
const jwt = new JoseJWTService(
async () => [{ id: 'my-key', value: config.jwtSecret }],
logger
)
return {
config,
logger,
jwt,
secrets: new LocalSecretService(),
variables: new LocalVariablesService(),
todoStore: new TodoStore(),
}
})
export const createWireServices = pikkuWireServices(
async (singletonServices, wire) => ({
scopedLogger: new ScopedLogger(wire.session?.initial?.userId),
})
)
// functions/todos.functions.ts — services are auto-injected
export const createTodo = pikkuFunc({
title: 'Create Todo',
func: async ({ todoStore, logger }, { title, priority }) => {
const todo = await todoStore.create(title, priority)
logger.info('Created todo', { id: todo.id })
return { todo }
},
})
documentation
Deprecated — use pikku-middleware instead. Tag middleware (addTagMiddleware) is now documented as a section within the pikku-middleware skill, alongside global HTTP middleware, execution order, and the service-to-service bearer auth pattern.
testing
Use when adding authorization checks to Pikku functions or routes — pikkuPermission, pikkuAuth, per-function permissions, pattern-based permissions, or understanding OR/AND permission logic. TRIGGER when: user wants to restrict who can call a function, check resource ownership, add role-based access, or understand where permission checks belong. DO NOT TRIGGER when: user asks about middleware or request interception (use pikku-middleware), authentication strategies (use pikku-security), or session management.
testing
Use when adding any middleware to a Pikku app — global HTTP middleware, tag-scoped middleware (including service-to-service bearer auth), per-route middleware, session-setting middleware, or understanding middleware execution order and priority. TRIGGER when: user wants middleware on some or all routes, machine-to-machine auth, tag-scoped cross-cutting concerns, global interceptors, or middleware priority/order questions. DO NOT TRIGGER when: user asks about permissions/authorization checks (use pikku-permissions), auth strategies like authBearer/authCookie (use pikku-security), or deployment.
documentation
Standard cleanup to run right after a Pikku template is cloned or scaffolded into a new project. TRIGGER when: a Pikku template was just cloned/scaffolded (via `pikku create`, `git clone <template>`, or the user says "I cloned the kanban template / starter / template"), or the working tree still looks like an untouched template (template README, placeholder `@project/*` name in package.json). DO NOT TRIGGER when: working in an established project mid-feature, or editing the template repo itself.