Tree-Shaking and Filtering
Pikku analyzes your wirings at build time to generate optimized entry points. This allows you to deploy the same codebase as a monolith, microservices, or individual functions without code changes.
How It Works
When you run pikku, the CLI:
- Scans your codebase for
pikkuFuncdefinitions and wirings (wireHTTP,wireChannel,wireQueueWorker, etc.) - Applies filters based on CLI arguments (
--http-routes,--tags,--types) - Determines required services by analyzing which services each filtered function uses
- Generates an entry point that imports only the needed functions and services
// Your code - write once
export const getUser = pikkuFunc({
func: async ({ database }) => { /* ... */ }
})
export const deleteUser = pikkuFunc({
func: async ({ database, audit }) => { /* ... */ }
})
export const getStatus = pikkuFunc({
func: async ({ cache }) => { /* ... */ }
})
wireHTTP({ route: '/users/:id', func: getUser })
wireHTTP({ route: '/admin/users/:id', func: deleteUser })
wireHTTP({ route: '/status', func: getStatus })
Deploying Everything
pikku
Generates an entry point that imports all three functions and instantiates all services: database, audit, cache.
Deploying Filtered Subset
pikku --http-routes=/status
Generates an entry point that:
- ✅ Imports only
getStatus - ✅ Instantiates only
cacheservice - ❌ Does NOT import
getUserordeleteUser - ❌ Does NOT instantiate
databaseorauditservices
This means your health check endpoint has zero database connection overhead.
Filter Types
By Route
pikku --http-routes=/admin
Include only functions wired to routes matching /admin.
By Tags
Tag your functions:
export const generateReport = pikkuFunc({
func: async ({ database }) => { /* ... */ },
docs: {
tags: ['reports', 'background']
}
})
Then filter:
pikku --tags=reports
By Protocol Type
pikku --types=http # Only HTTP handlers
pikku --types=queue # Only queue workers
pikku --types=http,queue # Multiple types
Combining Filters
pikku --http-routes=/admin --tags=payments
Filters use AND logic - functions must match all criteria.
Service Loading
Pikku analyzes which services your filtered functions actually use and generates optimized service initialization code. This happens in two ways:
1. Static Filtering (Recommended)
When services can be completely excluded, Pikku doesn't import them at all:
// Generated services.ts - only imports what's needed
import { createCache } from '../services/cache'
export const createServices = pikkuServices(async () => {
return {
cache: await createCache(),
// database and audit are NOT imported or loaded
}
})
2. Dynamic Filtering (For Shared Bundles)
When you need one bundle to serve multiple filtered deployments, Pikku uses conditional loading:
import { requiredSingletonServices } from './required-services.gen'
export const createServices = pikkuServices(async (config) => {
const logger = new ConsoleLogger()
const cache = new RedisCache()
// Only create JWT service if it's actually needed
let jwt: JWTService | undefined
if (requiredSingletonServices.jwt) {
const { JoseJWTService } = await import('@pikku/jose')
jwt = new JoseJWTService(
async () => [{ id: 'my-key', value: 'secret' }],
logger
)
}
// Only create database if needed
let database: Database | undefined
if (requiredSingletonServices.database) {
database = await createDatabase(config.databaseUrl)
}
return {
logger,
cache,
jwt,
database,
}
})
The pikkuServices wrapper ensures type safety and proper integration with Pikku's service system.
Why This Isn't More Magical
Unlike dependency injection frameworks like NestJS, Pikku:
- Doesn't use decorators or reflection - Just static analysis of your code
- Doesn't automatically wire dependencies - You explicitly declare what each function needs
- Doesn't have a runtime DI container - Services are created once at startup, passed to functions
This is intentional. It means:
- Faster cold starts - No reflection or decorator processing at runtime
- Explicit dependencies - Clear what each function needs
- Better tree-shaking - Bundlers can eliminate unused code
- Simpler mental model - No magic, just functions and imports
Architecture Evolution
The same codebase can be deployed differently as your needs change:
Monolith
Run everything in one process
Microservices
Split by domain or feature
Functions
One function per deployment
Phase 1: Monolith
pikku
Simple deployment, everything in one process.
Phase 2: Split Services
# Public API
pikku --tags=public
# Admin service
pikku --tags=admin
# Background workers
pikku --types=queue
Independent deployment and scaling per service.
Phase 3: Individual Functions
# One Lambda per critical endpoint
pikku --http-routes=/users/:id --types=http
Maximum isolation and optimization.
No code changes required.
What Pikku Generates
Pikku generates entry points with filtered imports. You then use your bundler of choice (esbuild, webpack, etc.) to create the final bundle.
// .pikku/entry.ts (generated)
import { getStatus } from '../src/functions/status'
import { createServices } from './services'
import { createHTTPHandler } from '@pikku/runtime-express'
const services = await createServices()
const handler = createHTTPHandler([
{ route: '/status', func: getStatus }
], services)
export default handler
The generated code is readable and straightforward - no magic.