Pikku
Pikku - meaning tiny ð in Finnish - is a minimalistic batteries included framework built around simple functions. It works with any event driven design (currently http, websockets and scheduled tasks) and can be run on any Javascript runtime, currently Cloudflare Workers, Fastify, Bun, AWS Lambda, Express, uWS, WS and others.
- HTTP APIs
- WebSocket
- Server-Sent Events
- RPC Services
- Server
- Client
import { pikkuFunc, addHTTPRoute } from '#pikku/pikku-types.gen.js'
export const createTodo = pikkuFunc<
{ title: string; description: string },
{ id: string; title: string; createdBy: string }
>(async ({ logger }, data, session) => {
// userSession is passed as 3rd parameter
const todo = {
id: crypto.randomUUID(),
title: data.title,
description: data.description,
createdBy: session.userId,
completed: false,
createdAt: new Date().toISOString()
}
logger.info(`Todo created by ${session.userId}`)
return todo
})
addHTTPRoute({
method: 'post',
route: '/todos',
func: createTodo,
auth: true, // Requires authentication
docs: {
description: 'Create a new todo',
tags: ['todos'],
},
})
import { createFetchClient } from '@pikku/client-fetch'
const api = createFetchClient({
baseUrl: process.env.API_BASE_URL,
token: userToken // Authentication token
})
// Type-safe API calls
const handleCreateTodo = async () => {
try {
// Full TypeScript intellisense and validation
const todo = await api.post('/todos', {
title: 'Learn Pikku',
description: 'Build my first Pikku application'
})
console.log(`Created todo: ${todo.title} by ${todo.createdBy}`)
return todo
} catch (error) {
console.error('Failed to create todo:', error)
throw error
}
}
- Server
- Client
import { pikkuChannelConnectionFunc, pikkuChannelFunc, addChannel } from '#pikku/pikku-types.gen.js'
export const onConnect = pikkuChannelConnectionFunc<{ welcome: string }>(
async ({ logger, channel }, session) => {
logger.info(`User ${session.userId} connected to chat`)
channel.send({
welcome: `Welcome ${session.username}!`
})
}
)
export const sendMessage = pikkuChannelFunc<
{ message: string },
{ timestamp: string; from: string; message: string }
>(async ({ channel, eventHub }, data, session) => {
// userSession provides authenticated user info
const chatMessage = {
timestamp: new Date().toISOString(),
from: session.username,
message: data.message
}
await eventHub?.publish('chat', channel.channelId, chatMessage)
return chatMessage
})
addChannel({
name: 'chat',
route: '/chat',
onConnect,
auth: true, // Requires authentication
onMessageRoute: {
action: {
sendMessage,
},
},
})
import { createWebSocketClient } from '@pikku/client-websocket'
const ws = createWebSocketClient({
baseUrl: process.env.WS_BASE_URL,
token: userToken // Authentication token
})
// Type-safe WebSocket communication
const handleChat = async () => {
try {
// Connect to authenticated channel
const channel = await ws.connect('/chat')
// Listen for messages with full type safety
channel.onMessage((message) => {
console.log(`${message.from}: ${message.message}`)
})
// Send type-safe messages
await channel.send({
action: 'sendMessage',
message: 'Hello from Pikku!'
})
} catch (error) {
console.error('Chat connection failed:', error)
}
}
- Server
- Client
import { pikkuFunc, addHTTPRoute } from '#pikku/pikku-types.gen.js'
export const streamUserActivity = pikkuFunc<
void,
AsyncGenerator<{ timestamp: string; activity: string; userId: string }>
>(async function* ({ logger }, data, session) {
// userSession provides authenticated user info
logger.info(`Starting activity stream for user ${session.userId}`)
let count = 0
while (true) {
yield {
timestamp: new Date().toISOString(),
activity: `User activity update #${++count}`,
userId: session.userId
}
await new Promise(resolve => setTimeout(resolve, 2000))
}
})
addHTTPRoute({
method: 'get',
route: '/stream/activity',
func: streamUserActivity,
auth: true, // Requires authentication
responseType: 'stream',
docs: {
description: 'Stream user activity updates',
tags: ['streaming'],
},
})
import { createSSEClient } from '@pikku/client-sse'
const sse = createSSEClient({
baseUrl: process.env.API_BASE_URL,
token: userToken // Authentication token
})
// Type-safe streaming
const handleActivityStream = async () => {
try {
// Connect to authenticated stream
const stream = await sse.connect('/stream/activity')
// Listen for streaming data with full type safety
stream.onMessage((update) => {
console.log(`${update.timestamp}: ${update.activity}`)
console.log(`User: ${update.userId}`)
})
stream.onError((error) => {
console.error('Stream error:', error)
})
// Stream will automatically reconnect on connection loss
} catch (error) {
console.error('Failed to start activity stream:', error)
}
}
- Server
- Client
import { pikkuSessionFunc, addRPCService } from '#pikku/pikku-types.gen.js'
export const getUserProfile = pikkuSessionFunc<
{ userId: string },
{ id: string; name: string; email: string; organizationId: string }
>(async ({ logger, userService }, { userId }, session) => {
// userSession provides authenticated user info
logger.info(`Getting profile for user ${userId} from org ${session.organizationId}`)
const user = await userService.findUserInOrganization({
userId,
organizationId: session.organizationId
})
if (!user) {
throw new Error('User not found or access denied')
}
return {
id: user.id,
name: user.name,
email: user.email,
organizationId: user.organizationId
}
})
addRPCService({
name: 'user-service',
version: '1.0.0',
methods: {
getUserProfile,
},
auth: true
})
import { createRPCClient } from '@pikku/client-rpc'
const userService = createRPCClient({
service: 'user-service',
version: '1.0.0',
baseUrl: process.env.API_BASE_URL,
token: userToken // Authentication token
})
// Type-safe service calls
const handleUserOperations = async (userId: string) => {
try {
// Get user profile with full type safety
const user = await userService.getUserProfile({
userId
})
console.log(`User: ${user.name} (${user.email})`)
console.log(`Organization: ${user.organizationId}`)
return user
} catch (error) {
console.error('User operation failed:', error)
throw error
}
}
Background Tasks & Queuesâ
Here are examples showing scheduled tasks and background queue processing:
- Scheduled Tasks
- Background Queues
import { pikkuVoidFunc, addScheduledTask } from '#pikku/pikku-types.gen.js'
export const dailyReportTask = pikkuVoidFunc(
async ({ logger, emailService, analyticsService }) => {
try {
// Generate daily metrics report
const metrics = await analyticsService.getDailyMetrics()
// Send report to admin team
await emailService.sendReport({
to: 'admin@company.com',
subject: 'Daily Metrics Report',
data: {
totalUsers: metrics.activeUsers,
totalRequests: metrics.apiCalls,
avgResponseTime: metrics.avgResponseTime,
date: new Date().toISOString().split('T')[0]
}
})
logger.info(`Daily report sent - ${metrics.activeUsers} active users`)
} catch (error) {
logger.error('Failed to generate daily report:', error)
throw error // Will trigger retry logic
}
}
)
addScheduledTask({
name: 'dailyReport',
schedule: '0 8 * * *', // Daily at 8 AM
func: dailyReportTask,
timezone: 'America/New_York',
docs: {
description: 'Generate and send daily metrics report',
tags: ['reports', 'analytics'],
},
})
import { pikkuSessionlessFunc, addQueueWorker } from '#pikku/pikku-types.gen.js'
export const processImageResize = pikkuSessionlessFunc<
{ imageUrl: string; sizes: number[]; userId: string },
{ processedImages: Array<{ size: number; url: string }> }
>(async ({ logger, imageService, storageService }, data) => {
try {
logger.info(`Processing image resize for user ${data.userId}`)
const processedImages = []
for (const size of data.sizes) {
// Resize image to specified size
const resizedBuffer = await imageService.resize(data.imageUrl, size)
// Upload to storage
const uploadedUrl = await storageService.upload({
buffer: resizedBuffer,
filename: `${data.userId}/resized_${size}px.jpg`,
contentType: 'image/jpeg'
})
processedImages.push({ size, url: uploadedUrl })
}
logger.info(`Successfully processed ${processedImages.length} image sizes`)
return { processedImages }
} catch (error) {
logger.error('Failed to process image:', error)
throw error // Will trigger retry logic
}
})
addQueueWorker({
name: 'image-processing',
func: processImageResize,
concurrency: 3,
retryOptions: {
attempts: 3,
backoff: 'exponential',
delay: 2000,
},
})
Quick Startâ
- npm
- Yarn
- pnpm
- Bun
npm create pikku@latest
npm create pikku@latest
# couldn't auto-convert command
npm create pikku@latest
# couldn't auto-convert command
bunx create-pikku@latest
Featuresâ
- Tiny ð - The Pikku footprint is minimal. It currently relies on two small dependencies cookie and path-to-regex.
- Deploy Anywhere ð - Run your functions anywhere, as long as it's event based. Currently supporting HTTP, WebSockets and Scheduled Tasks with more coming soon.
- Batteries Included ð - Services, session management, auth, docs, it's all here..
- Zero Boilerplate ðŠķ - Only write business logic in Typescript, Pikku automatically optimizes, validates and generates everything needed in a powerful framework.