Skip to content

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.

Bash
npm install @surfjs/core express

Basic Setup

TypeScript
// server.ts
import { 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:

TypeScript
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:

TypeScript
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:

TypeScript
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 middleware
app.use(helmet())
app.use(express.json())
app.use(rateLimit({ windowMs: 60_000, max: 100 }))
 
// Existing routes
app.get('/health', (_req, res) => res.json({ ok: true }))
app.use('/api/v1', myExistingRouter)
 
// Surf routes mounted alongside your API
app.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:

TypeScript
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:

JSON
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"strict": true
}
}

Run with tsx during development for zero-config TypeScript:

Bash
npx tsx server.ts