Most Node framework code should not know how the workbook is built. Keep the
spreadsheet logic behind one web-standard Request -> Response handler, then
adapt the framework edge around it.
The runnable example is in
examples/serverless-workpaper-api.
It builds a small revenue workbook, writes records into a Revenue sheet,
reads summary formulas, saves the WorkPaper document JSON, and verifies that
the computed total survives the framework boundary.
git clone https://github.com/proompteng/bilig.git
cd bilig/examples/serverless-workpaper-api
npm install
npm run framework-adapters
Expected output:
{
"adapters": ["fetch", "hono", "oak", "adonis", "hapi", "express", "fastify"],
"before": {
"fetch": {
"totalRevenue": 36900,
"westCustomers": 20,
"largestDeal": 24000
},
"hono": {
"totalRevenue": 36900,
"westCustomers": 20,
"largestDeal": 24000
},
"oak": {
"totalRevenue": 36900,
"westCustomers": 20,
"largestDeal": 24000
},
"adonis": {
"totalRevenue": 36900,
"westCustomers": 20,
"largestDeal": 24000
},
"hapi": {
"totalRevenue": 36900,
"westCustomers": 20,
"largestDeal": 24000
}
},
"oak": {
"status": 200,
"edit": {
"records": 4,
"after": {
"totalRevenue": 48600,
"westCustomers": 20,
"largestDeal": 24000
},
"checks": {
"totalRevenueChanged": true,
"formulasPersisted": true,
"serializedBytes": 1195
}
}
},
"adonis": {
"status": 200,
"edit": {
"records": 4,
"after": {
"totalRevenue": 48600,
"westCustomers": 20,
"largestDeal": 24000
},
"checks": {
"totalRevenueChanged": true,
"formulasPersisted": true,
"serializedBytes": 1195
}
}
},
"hapi": {
"status": 200,
"edit": {
"records": 4,
"after": {
"totalRevenue": 48600,
"westCustomers": 20,
"largestDeal": 24000
},
"checks": {
"totalRevenueChanged": true,
"formulasPersisted": true,
"serializedBytes": 1195
}
}
},
"express": {
"status": 200,
"edit": {
"records": 4,
"after": {
"totalRevenue": 48600,
"westCustomers": 20,
"largestDeal": 24000
},
"checks": {
"totalRevenueChanged": true,
"formulasPersisted": true,
"serializedBytes": 1195
}
}
},
"fastify": {
"status": 200,
"summary": {
"totalRevenue": 48600,
"westCustomers": 20,
"largestDeal": 24000
}
},
"verified": true
}
The example keeps the WorkPaper handler framework-neutral:
import { handleWorkPaperRequest } from './route.ts'
export const GET = handleWorkPaperRequest
export const POST = handleWorkPaperRequest
That shape works directly in Fetch-style runtimes and is easy to wrap in frameworks that use their own request and response objects.
For App Router endpoints that accept JSON, keep the Next-specific file thin and
return web-standard Response objects. The runnable example proves the route
can parse JSON, update an input cell, read back a dependent formula, and reload
the persisted WorkPaper document:
cd examples/serverless-workpaper-api
npm install
npm run test
The copyable route shape is:
export const dynamic = 'force-dynamic'
export const runtime = 'nodejs'
export async function POST(request: Request) {
const { customers } = await request.json()
const result = updateRevenueInputCell(Number(customers))
return Response.json({
input: { cell: 'Inputs!B2', customers: result.customers },
formulaReadback: { cell: 'Summary!B2', revenue: result.revenue },
persistence: result.persistence,
})
}
Use this for Next.js route handlers; use the generic adapters below when the framework gives you Express, Fastify, Hono, Oak, AdonisJS, Hapi, or another request wrapper.
import express from 'express'
import { createExpressWorkPaperHandler } from './framework-adapters.ts'
const app = express()
app.use(express.json())
app.get('/api/workpaper/summary', createExpressWorkPaperHandler())
app.post('/api/workpaper/revenue', createExpressWorkPaperHandler())
app.listen(8787)
import Fastify from 'fastify'
import { createFastifyWorkPaperHandler } from './framework-adapters.ts'
const app = Fastify()
const workpaper = createFastifyWorkPaperHandler()
app.get('/api/workpaper/summary', workpaper)
app.post('/api/workpaper/revenue', workpaper)
await app.listen({ port: 8787 })
import { Hono } from 'hono'
import { createHonoWorkPaperHandler } from './framework-adapters.ts'
const app = new Hono()
const workpaper = createHonoWorkPaperHandler()
app.get('/api/workpaper/summary', workpaper)
app.post('/api/workpaper/revenue', workpaper)
export default app
import { Application, Router } from '@oak/oak'
import { createOakWorkPaperRoutes } from './framework-adapters.ts'
const app = new Application()
const router = new Router()
const [summaryRoute, revenueRoute] = createOakWorkPaperRoutes()
router.get(summaryRoute.path, summaryRoute.handler)
router.post(revenueRoute.path, revenueRoute.handler)
app.use(router.routes())
app.use(router.allowedMethods())
await app.listen({ port: 8787 })
import router from '@adonisjs/core/services/router'
import { createAdonisWorkPaperRoutes } from './framework-adapters.ts'
const [summaryRoute, revenueRoute] = createAdonisWorkPaperRoutes()
router.get(summaryRoute.path, summaryRoute.handler)
router.post(revenueRoute.path, revenueRoute.handler)
import Hapi from '@hapi/hapi'
import { createHapiWorkPaperRoutes } from './framework-adapters.ts'
const server = Hapi.server({ port: 8787 })
server.route([...createHapiWorkPaperRoutes()])
await server.start()
The adapter should do only four things:
The workbook logic stays in
route.ts.
The adapters live in
framework-adapters.ts.
Run npm run smoke and npm run framework-adapters before moving the handler
into your own service.