Skip to main content

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:

  1. Scans your codebase for pikkuFunc definitions and wirings (wireHTTP, wireChannel, wireQueueWorker, etc.)
  2. Applies filters based on CLI arguments (--http-routes, --tags, --types)
  3. Determines required services by analyzing which services each filtered function uses
  4. 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 cache service
  • ❌ Does NOT import getUser or deleteUser
  • ❌ Does NOT instantiate database or audit services

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:

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:

  1. Faster cold starts - No reflection or decorator processing at runtime
  2. Explicit dependencies - Clear what each function needs
  3. Better tree-shaking - Bundlers can eliminate unused code
  4. 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

pikku
~2.8MB
All functions, all protocols
📦

Microservices

Split by domain or feature

pikku --http-routes=/admin
pikku --tags=admin
~180KB
Only admin routes + dependencies
λ

Functions

One function per deployment

pikku --http-routes=/users/:id --types=http
~50KB
Single endpoint + minimal runtime

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.