The Basics
Function to endpoint in two lines
Define your function once, wire it to a route. Pikku handles the rest.
const getBook = pikkuFunc({
title: 'Get Book',
description: 'Retrieve a book by ID',
func: async ({ db }, { bookId }) => {
return await db.getBook(bookId)
},
permissions: { user: isAuthenticated }
})
wireHTTP({
method: 'get',
route: '/books/:bookId',
func: getBook
})
Path params extracted
:bookId becomes a typed input field automatically
Errors mapped to status codes
Throw NotFoundError → 404, UnauthorizedError → 401
OpenAPI generated
Every wired route appears in your OpenAPI spec for free
Data Flow
Everything merges into one typed input
Path params, query strings, and request bodies combine into a single object your function receives.
Path Params
:bookId → "42"
Query String
?format=pdf
Request Body
{ title: "..." }
Merged Input
{ bookId, format, title }wireHTTP({
method: 'post',
route: '/books/:bookId',
func: updateBook
})
// POST /books/42?format=pdf
// Body: { title: "New Title" }
//
// → updateBook receives:
// { bookId: "42", format: "pdf", title: "New Title" }
Same key in multiple sources with different values? Pikku throws a validation error — no silent overwrites.
Auth & Permissions
Auth and permissions, everywhere
Every HTTP route inherits Pikku's auth system. Override per-route or apply globally.
Per-route auth control
Set auth: false on public routes. Everything else requires a valid session by default.
Permission guards
Attach permission checks to individual routes. Pikku rejects unauthorized requests before your function runs.
Global policies
Use addHTTPPermission to apply rules to entire path prefixes — every route under /admin requires admin access.
// Public route — no auth required
wireHTTP({
method: 'get',
route: '/books',
func: listBooks,
auth: false
})
// Protected with permissions
wireHTTP({
method: 'delete',
route: '/books/:bookId',
func: deleteBook,
permissions: {
admin: isAdmin
}
})
// Global route-level permission
addHTTPPermission(
'/admin/*',
{ admin: isAdmin }
)
Middleware
Hooks at every level
Apply middleware globally, by path prefix, or on individual routes. They run in onion order — outer middleware wraps inner.
Global
*addHTTPMiddleware('*', [...]) — runs on every HTTP request. CORS, logging, auth.
Prefix-based
/prefixaddHTTPMiddleware('/api/*', [...]) — scoped to a path prefix. Rate limiting, admin guards.
Per-route
routemiddleware: [...] on a single wireHTTP call. Audit trails, special validation.
Built-in middleware: cors, authBearer, authCookie, authAPIKey — all from @pikku/core/middleware.
import { cors, authBearer } from '@pikku/core/middleware'
// Global: CORS + bearer auth on every route
addHTTPMiddleware('*', [
cors({ origin: 'https://app.example.com', credentials: true }),
authBearer()
])
// Prefix: rate limiting on API routes only
addHTTPMiddleware('/api/*', [
rateLimit({ maxRequests: 100, windowMs: 60_000 })
])
// Per-route: audit logging on a single wire
wireHTTP({
method: 'delete',
route: '/books/:bookId',
func: deleteBook,
middleware: [auditLog]
})
Route Groups
Organize routes with defineHTTPRoutes
Group related routes into contracts. Compose them with shared base paths, middleware, and auth settings — then wire them all at once.
import { defineHTTPRoutes, wireHTTPRoutes } from '.pikku/pikku-types.gen.js'
const booksRoutes = defineHTTPRoutes({
tags: ['books'],
routes: {
list: { method: 'get', route: '/books', func: listBooks, auth: false },
get: { method: 'get', route: '/books/:bookId', func: getBook },
create: { method: 'post', route: '/books', func: createBook },
delete: { method: 'delete', route: '/books/:bookId', func: deleteBook },
},
})
const todosRoutes = defineHTTPRoutes({
auth: false,
tags: ['todos'],
routes: {
list: { method: 'get', route: '/todos', func: listTodos },
create: { method: 'post', route: '/todos', func: createTodo },
get: { method: 'get', route: '/todos/:id', func: getTodo },
},
})
// Compose everything under /api/v1
wireHTTPRoutes({
basePath: '/api/v1',
middleware: [cors()],
routes: {
books: booksRoutes,
todos: todosRoutes,
},
})
Config cascades down
basePath, tags, and middleware from the group apply to every route inside it
Routes can override
Set auth: false on a single route even if the group requires auth
Compose contracts
Define route groups in separate files, import and compose them in one wireHTTPRoutes call
Type-Safe Client
Call your API with full IntelliSense
Pikku generates a typed fetch client from your HTTP wirings. Every route, every input, every return type — autocompleted.
import { pikkuFetch } from '.pikku/pikku-fetch.gen.js'
pikkuFetch.setServerUrl('http://localhost:4002')
// Fully typed — route, input, and output are inferred
const books = await pikkuFetch.get('/api/v1/books', {})
const book = await pikkuFetch.get('/api/v1/books/:bookId', {
bookId: '42'
})
const created = await pikkuFetch.post('/api/v1/books', {
title: 'The Pikku Guide',
author: 'You'
})
// Auth: set a JWT and all subsequent requests include it
pikkuFetch.setAuthorizationJWT(token)
const deleted = await pikkuFetch.delete('/api/v1/books/:bookId', {
bookId: created.bookId
})
Generated from your wirings
Run npx @pikku/cli fetch and get a PikkuFetch class with typed overloads for every HTTP route you've wired.
Auth built in
setAuthorizationJWT(), setAPIKey() — set once, included on every request.
Works everywhere
Built on the Fetch API — works in the browser, Node, Deno, Next.js server components, or anywhere that has fetch.
Server-Sent Events
When you need streaming, just add sse: true
The same route serves both regular HTTP clients and SSE clients. Non-SSE clients get a JSON response, SSE clients get a stream.
{
"todos": [
{ "id": 1, "text": "Buy milk" },
{ "id": 2, "text": "Write docs" },
{ "id": 3, "text": "Ship feature" },
{ "id": 4, "text": "Deploy app" }
]
}wireHTTP({
method: 'get',
route: '/todos',
func: getTodos,
sse: true // ← that's it
})
const getTodos = pikkuFunc({
title: 'Get Todos',
func: async ({ db, channel }, {}) => {
const todos = await db.getTodos()
// If client supports SSE, stream them
if (channel) {
for (const todo of todos) {
channel.send({ todo })
await sleep(100)
}
return
}
// Otherwise, return the full list
return { todos }
}
})
Start wiring HTTP in 5 minutes
One command to scaffold a project with HTTP wiring already configured.
MIT Licensed · Works with Express, Fastify, Lambda & Cloudflare