bilig

Express, Fastify, Hono, Oak, Hapi, and AdonisJS adapters for a WorkPaper API

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.

Run the adapter smoke

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
}

Shared route shape

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.

Next.js Route Handler JSON

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.

Express

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)

Fastify

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 })

Hono

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

Oak

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 })

AdonisJS

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)

Hapi

import Hapi from '@hapi/hapi'
import { createHapiWorkPaperRoutes } from './framework-adapters.ts'

const server = Hapi.server({ port: 8787 })

server.route([...createHapiWorkPaperRoutes()])

await server.start()

What the wrapper must preserve

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.