Skip to content

Custom Adapters

Deploy EreoJS to different runtimes and platforms.

Overview

EreoJS is built on standard Web APIs and can run on any platform that supports them. Adapters provide the glue between EreoJS and specific runtimes.

Built-in Adapters

Bun Adapter (Default)

ts
import { defineConfig } from '@ereo/core'

export default defineConfig({
  build: {
    target: 'bun'  // Default
  }
})

Cloudflare Workers

ts
export default defineConfig({
  build: {
    target: 'cloudflare'
  }
})

Node.js

ts
export default defineConfig({
  build: {
    target: 'node'
  }
})

Deno

ts
export default defineConfig({
  build: {
    target: 'deno'
  }
})

Creating a Custom Adapter

Adapter Interface

ts
interface Adapter {
  name: string
  build?: (options: BuildOptions) => Promise<void>
  serve: (app: EreoApp, options: ServeOptions) => Server | Promise<Server>
}

interface BuildOptions {
  outDir: string
  minify: boolean
  sourcemap: boolean
}

interface ServeOptions {
  port: number
  hostname: string
}

Basic Adapter Example

ts
// adapters/custom.ts
import type { Adapter, EreoApp } from '@ereo/core'

export function customAdapter(): Adapter {
  return {
    name: 'custom-adapter',

    async build({ outDir, minify }) {
      // Custom build logic
      console.log(`Building to ${outDir}`)
    },

    serve(app, { port, hostname }) {
      // Create server using the app's fetch handler
      const server = createServer(async (req, res) => {
        const request = toWebRequest(req)
        const response = await app.handle(request)
        writeResponse(res, response)
      })

      server.listen(port, hostname)
      return server
    }
  }
}

Using the Adapter

ts
// ereo.config.ts
import { defineConfig } from '@ereo/core'
import { customAdapter } from './adapters/custom'

export default defineConfig({
  adapter: customAdapter()
})

Platform-Specific Adapters

AWS Lambda

ts
// adapters/lambda.ts
import type { Adapter, EreoApp } from '@ereo/core'
import type { APIGatewayProxyEvent, Context } from 'aws-lambda'

export function lambdaAdapter(): Adapter {
  return {
    name: 'lambda',

    async build({ outDir }) {
      // Bundle for Lambda
      await Bun.build({
        entrypoints: ['./src/lambda-handler.ts'],
        outdir: outDir,
        target: 'node',
        minify: true
      })
    },

    serve(app) {
      // Return Lambda handler instead of server
      return async (event: APIGatewayProxyEvent, context: Context) => {
        const request = lambdaEventToRequest(event)
        const response = await app.handle(request)
        return responseToLambda(response)
      }
    }
  }
}

function lambdaEventToRequest(event: APIGatewayProxyEvent): Request {
  const url = `https://${event.headers.host}${event.path}`
  const searchParams = new URLSearchParams(event.queryStringParameters || {})

  return new Request(`${url}?${searchParams}`, {
    method: event.httpMethod,
    headers: new Headers(event.headers as Record<string, string>),
    body: event.body
  })
}

function responseToLambda(response: Response) {
  return {
    statusCode: response.status,
    headers: Object.fromEntries(response.headers),
    body: response.body,
    isBase64Encoded: false
  }
}

Fastify

ts
// adapters/fastify.ts
import Fastify from 'fastify'
import type { Adapter, EreoApp } from '@ereo/core'

export function fastifyAdapter(): Adapter {
  return {
    name: 'fastify',

    serve(app, { port, hostname }) {
      const fastify = Fastify()

      fastify.all('*', async (request, reply) => {
        const webRequest = new Request(
          `http://${request.hostname}${request.url}`,
          {
            method: request.method,
            headers: request.headers as Record<string, string>,
            body: request.body ? JSON.stringify(request.body) : undefined
          }
        )

        const response = await app.handle(webRequest)

        reply
          .status(response.status)
          .headers(Object.fromEntries(response.headers))
          .send(await response.text())
      })

      fastify.listen({ port, host: hostname })
      return fastify.server
    }
  }
}

Express

ts
// adapters/express.ts
import express from 'express'
import type { Adapter, EreoApp } from '@ereo/core'

export function expressAdapter(): Adapter {
  return {
    name: 'express',

    serve(app, { port, hostname }) {
      const server = express()

      server.use(async (req, res) => {
        const protocol = req.protocol
        const host = req.get('host')
        const url = `${protocol}://${host}${req.originalUrl}`

        const webRequest = new Request(url, {
          method: req.method,
          headers: req.headers as Record<string, string>,
          body: ['GET', 'HEAD'].includes(req.method) ? undefined : req.body
        })

        const response = await app.handle(webRequest)

        res.status(response.status)
        response.headers.forEach((value, key) => res.set(key, value))
        res.send(await response.text())
      })

      return server.listen(port, hostname)
    }
  }
}

Adapter Hooks

Adapters can hook into the build and serve lifecycle:

ts
export function myAdapter(): Adapter {
  return {
    name: 'my-adapter',

    // Called before build starts
    async beforeBuild(options) {
      console.log('Preparing build...')
    },

    // Main build logic
    async build(options) {
      // Build application
    },

    // Called after build completes
    async afterBuild(options, result) {
      console.log(`Built in ${result.duration}ms`)
    },

    // Called before server starts
    async beforeServe(app, options) {
      console.log('Starting server...')
    },

    // Main serve logic
    serve(app, options) {
      return server
    },

    // Called after server starts
    async afterServe(server) {
      console.log('Server ready')
    }
  }
}

Static Asset Handling

Handle static assets in your adapter:

ts
serve(app, { port, hostname }) {
  const server = Bun.serve({
    port,
    hostname,
    async fetch(request) {
      const url = new URL(request.url)

      // Serve static files
      if (url.pathname.startsWith('/static/')) {
        const file = Bun.file(`./dist${url.pathname}`)
        if (await file.exists()) {
          return new Response(file)
        }
      }

      // Handle with EreoJS
      return app.handle(request)
    }
  })

  return server
}

Environment Detection

Detect runtime environment:

ts
function detectRuntime() {
  if (typeof Bun !== 'undefined') return 'bun'
  if (typeof Deno !== 'undefined') return 'deno'
  if (typeof process !== 'undefined' && process.versions?.node) return 'node'
  if (typeof globalThis.caches !== 'undefined') return 'cloudflare'
  return 'unknown'
}

Released under the MIT License.