.claude/skills/pikku-http/SKILL.md
Use when adding HTTP routes, REST APIs, web endpoints, or SSE streams to a Pikku app. Covers wireHTTP, defineHTTPRoutes, route groups, auth, middleware, permissions, SSE, and generated fetch client. TRIGGER when: code uses wireHTTP/defineHTTPRoutes/wireHTTPRoutes, user asks about REST endpoints, API routes, SSE, or the generated fetch client. DO NOT TRIGGER when: user asks about WebSocket (use pikku-websocket), queue workers (use pikku-queue), or deployment (use pikku-deploy-*).
npx skillsauth add pikkujs/pikku pikku-httpInstall 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.
Wire Pikku functions to HTTP endpoints. Supports single routes, composable route groups, auth, middleware, permissions, SSE, and auto-generated type-safe clients.
Run these commands to understand the current project:
pikku info functions --verbose # See existing functions, their types, tags, middleware
pikku info tags --verbose # Understand project organization and naming conventions
pikku info middleware --verbose # See what middleware is already applied
Follow existing patterns you find (naming, tag usage, file organization). See pikku-concepts for the core mental model.
wireHTTP(config)Wire a single function to an HTTP endpoint.
import { wireHTTP } from '@pikku/core/http'
wireHTTP({
method: 'get' | 'post' | 'put' | 'patch' | 'delete' | 'head',
route: string, // e.g. '/books/:bookId' — :params become data fields
func: PikkuFunc, // The function to call
auth?: boolean, // Override default auth (true = require session)
tags?: string[], // For grouping, middleware targeting
permissions?: Record<string, PikkuPermission | PikkuPermission[]>,
middleware?: PikkuMiddleware[],
sse?: boolean, // Enable Server-Sent Events
contentType?: 'xml' | 'json', // Response content type
timeout?: number, // Request timeout in ms
headers?: HTTPHeadersSchema, // Expected headers schema
docs?: HTTPRouteDocsConfig, // OpenAPI docs config
})
defineHTTPRoutes(config) + wireHTTPRoutes(config)Group routes with shared configuration. Groups are composable and nestable.
import { defineHTTPRoutes, wireHTTPRoutes } from '.pikku/pikku-types.gen.js'
const routes = defineHTTPRoutes({
basePath?: string, // Prepended to all route paths
tags?: string[], // Applied to all routes in group
auth?: boolean, // Default auth for all routes (overridable per-route)
middleware?: PikkuMiddleware[],
routes: {
[key: string]: {
method: string,
route: string,
func: PikkuFunc,
auth?: boolean, // Override group auth
permissions?: Record<string, PikkuPermission | PikkuPermission[]>,
middleware?: PikkuMiddleware[],
}
}
})
wireHTTPRoutes({
basePath?: string, // Top-level prefix (e.g. '/api/v1')
middleware?: PikkuMiddleware[],
routes: {
[key: string]: ReturnType<typeof defineHTTPRoutes>,
}
})
Config cascading rules:
basePath — concatenates down the chaintags — merge (union)auth — child overrides parentaddHTTPMiddleware(pattern, middlewares)addHTTPMiddleware('*', [authBearer()]) // All routes
addHTTPMiddleware('/api/*', [rateLimit()]) // Pattern match
addHTTPPermission(pattern, permissions)addHTTPPermission('/admin/*', { admin: [isAdmin] })
Pikku merges route params, query params, and request body into a single data object:
// POST /books/42?format=pdf with body { title: "New Title" }
wireHTTP({ method: 'post', route: '/books/:bookId', func: updateBook })
// → updateBook receives: { bookId: "42", format: "pdf", title: "New Title" }
wireHTTP({
method: 'get',
route: '/books/:bookId',
func: getBook,
})
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 },
},
})
wireHTTPRoutes({
basePath: '/api/v1',
middleware: [cors()],
routes: {
books: booksRoutes,
todos: todosRoutes,
},
})
// Results in: GET /api/v1/books, POST /api/v1/books, etc.
// Public route (no auth)
wireHTTP({ method: 'get', route: '/books', func: listBooks, auth: false })
// Route with permission check
wireHTTP({
method: 'delete',
route: '/books/:bookId',
func: deleteBook,
permissions: { admin: isAdmin },
})
// Pattern-based permissions
addHTTPPermission('/admin/*', { admin: isAdmin })
import { cors, authBearer } from '@pikku/core/middleware'
// Global middleware
addHTTPMiddleware('*', [
cors({ origin: 'https://app.example.com', credentials: true }),
authBearer(),
])
// Scoped middleware
addHTTPMiddleware('/api/*', [rateLimit({ maxRequests: 100, windowMs: 60_000 })])
// Per-route middleware
wireHTTP({
method: 'delete',
route: '/books/:bookId',
func: deleteBook,
middleware: [auditLog],
})
wireHTTP({
method: 'get',
route: '/todos',
func: getTodos,
sse: true,
})
const getTodos = pikkuFunc({
title: 'Get Todos',
func: async ({ db, channel }, {}) => {
const todos = await db.getTodos()
if (channel) {
for (const todo of todos) {
channel.send({ todo })
await sleep(100)
}
return
}
return { todos }
},
})
After npx pikku prebuild, a type-safe client is generated:
import { pikkuFetch } from '.pikku/pikku-fetch.gen.js'
pikkuFetch.setServerUrl('http://localhost:4002')
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',
})
pikkuFetch.setAuthorizationJWT(token)
const deleted = await pikkuFetch.delete('/api/v1/books/:bookId', {
bookId: created.bookId,
})
// functions/books.functions.ts
import { pikkuFunc, pikkuSessionlessFunc } from '#pikku'
export const listBooks = pikkuSessionlessFunc({
title: 'List Books',
func: async ({ db }, { limit }) => {
return { books: await db.listBooks(limit) }
},
})
export const getBook = pikkuFunc({
title: 'Get Book',
description: 'Retrieve a book by ID',
func: async ({ db }, { bookId }) => {
return await db.getBook(bookId)
},
permissions: { user: isAuthenticated },
})
export const createBook = pikkuFunc({
title: 'Create Book',
func: async ({ db }, { title, author }) => {
return await db.createBook({ title, author })
},
})
export const deleteBook = pikkuFunc({
title: 'Delete Book',
func: async ({ db }, { bookId }) => {
await db.deleteBook(bookId)
return { deleted: true }
},
})
// wirings/books.http.ts
import { defineHTTPRoutes, wireHTTPRoutes } from '.pikku/pikku-types.gen.js'
import { addHTTPMiddleware } from '@pikku/core/http'
import { cors, authBearer } from '@pikku/core/middleware'
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 },
},
})
wireHTTPRoutes({
basePath: '/api',
routes: { books: booksRoutes },
})
addHTTPMiddleware('*', [cors(), authBearer()])
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.
development
Make a Pikku frontend work in both English (LTR) and Arabic / right-to-left languages. Direction is derived from the active locale, applied once at the document root, and the layout mirrors itself — but only if styling is written flow-relative (margin-inline-start, text-align: start, Mantine ms/me) instead of left/right. TRIGGER when: adding Arabic (or Hebrew/Farsi/Urdu), asked to "support RTL / right-to-left / bidi / mirror the layout", or writing layout styles in an app that may run RTL. Builds on pikku-i18n (an RTL language is just another locale file). DO NOT TRIGGER for backend functions or for LTR-only copy changes.
development
Wire i18n into a Pikku frontend (Vite SPA, Vite SSR, or Next.js app-router) with react-i18next + i18next. English by default, every user-facing string goes through a `t()` token, and additional languages are served under `/de` `/es` URL prefixes. TRIGGER when: scaffolding or editing a frontend and writing user-facing text, adding a second language, or asked to "make this translatable / use tokens / add i18n". DO NOT TRIGGER for backend functions, error messages thrown from functions, or log output.
development
Use when translating an n8n Code node body into a real Pikku function body. Triggered when the user opens or points at a stub generated by @pikku/n8n-import (look for `STUB — generated from n8n Code node` in the file's JSDoc), or when the user says 'translate this n8n code', 'port this n8n code node', 'finish the codeStub__... function', etc. The stub file is a `pikkuSessionlessFunc` with a Zod input/output, a JSDoc preserving the original n8n JavaScript verbatim, and a `throw new Error('… — implement me')` body.