Framework Adapters
Express / Node.js
Express and Node.js HTTP adapter
Express / Node.js#
The built-in middleware() method returns a standard Node.js HTTP handler compatible with Express, Connect, and raw http.createServer.
npm install @surfjs/core expressBasic Setup
// server.tsimport { createSurf } from '@surfjs/core'import express from 'express' const surf = await createSurf({ name: 'My API', commands: { search: { description: 'Search products by keyword', params: { query: { type: 'string', required: true }, limit: { type: 'number', default: 10 }, }, run: async ({ query, limit }) => { return db.products.search(query, { limit }) }, }, },}) const app = express()app.use(express.json())app.use(surf.middleware()) // Mounts all Surf routes app.listen(3000, () => { console.log('Server running at http://localhost:3000')})Routes mounted by surf.middleware():
| Method | Path | Description |
|--------|------|-------------|
| GET | /.well-known/surf.json | Manifest (with ETag/304 caching) |
| POST | /surf/execute | Command execution |
| POST | /surf/pipeline | Pipeline execution |
| POST | /surf/session/start | Start a session |
| POST | /surf/session/end | End a session |
With Authentication
Pass an authVerifier function to createSurf. The middleware extracts the Authorization header automatically:
import { createSurf, bearerVerifier } from '@surfjs/core'import express from 'express' const surf = await createSurf({ name: 'Protected API', auth: { type: 'bearer', description: 'API token' }, authVerifier: async (token, command) => { const user = await db.users.findByToken(token) if (!user) return { valid: false, reason: 'Invalid token' } return { valid: true, claims: { userId: user.id, plan: user.plan } } }, commands: { 'orders.list': { description: 'List orders for the authenticated user', auth: 'required', run: async (_params, ctx) => { return db.orders.findByUser(ctx.claims!.userId) }, }, 'catalog.search': { description: 'Search the product catalog (public)', params: { query: { type: 'string', required: true } }, run: async ({ query }) => db.products.search(query), }, },}) const app = express()app.use(express.json())app.use(surf.middleware())app.listen(3000)Client Configuration for Custom Paths
If your Surf routes are behind a reverse proxy or custom path, configure the client:
const client = await SurfClient.discover('https://myapi.com', { basePath: '/api/v2/surf/execute',})Composing with Existing Middleware
Surf plays well with your existing Express setup. Mount it alongside your other routes:
import { createSurf } from '@surfjs/core'import express from 'express'import helmet from 'helmet'import rateLimit from 'express-rate-limit' const surf = await createSurf({ /* ... */ }) const app = express() // Existing middlewareapp.use(helmet())app.use(express.json())app.use(rateLimit({ windowMs: 60_000, max: 100 })) // Existing routesapp.get('/health', (_req, res) => res.json({ ok: true }))app.use('/api/v1', myExistingRouter) // Surf routes mounted alongside your APIapp.use(surf.middleware()) app.listen(3000)Raw Node.js HTTP Server
No Express? surf.middleware() returns a standard Node.js (req, res, next) handler, so you can use it with http.createServer directly:
import { createSurf } from '@surfjs/core'import { createServer, IncomingMessage, ServerResponse } from 'node:http' const surf = await createSurf({ /* ... */ })const handler = surf.middleware() const server = createServer((req: IncomingMessage, res: ServerResponse) => { handler(req, res, () => { // Surf handles all its own routes (including 404s for unknown Surf paths). // This fallback only runs for non-Surf routes (e.g. /favicon.ico). res.writeHead(404, { 'Content-Type': 'application/json' }) res.end(JSON.stringify({ error: 'Not Found' })) })}) server.listen(3000)TypeScript Configuration
Ensure your tsconfig.json targets a modern Node.js version to avoid issues with top-level await:
{ "compilerOptions": { "target": "ES2022", "module": "Node16", "moduleResolution": "Node16", "strict": true }}Run with tsx during development for zero-config TypeScript:
npx tsx server.ts