npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@geek-fun/serverless-adapter

v0.2.9

Published

Adapter for web frame work express, koa, springboot to run in serverless function as backend of apigateway cross multi cloud provider like aliyun, huawei

Readme

Serverless-Adapter

Node.js CI release npm version Known Vulnerabilities License codecov

Adapter for web frameworks (Express, Koa, Hono) to run on serverless platforms across multiple cloud providers with automatic provider detection.

Supported Cloud Providers

| Provider | Service | Status | Trigger Type | | ---------------------- | ------------------------------- | ------------ | ------------ | | Alibaba Cloud (Aliyun) | Function Compute | ✅ Supported | API Gateway | | Tencent Cloud | Serverless Cloud Function (SCF) | ✅ Supported | Function URL | | Volcengine | veFaaS (函数服务) | ✅ Supported | API Gateway | | AWS | Lambda + API Gateway | ✅ Supported | API Gateway (REST API v1 & HTTP API v2) | | Cloudflare | Workers | ✅ Supported | fetch (Request → Response) |

Note: Tencent retired the SCF API Gateway trigger (service ended 2025-06-30); this adapter supports the Function URL (函数 URL) event-function format. Web 函数 (Web functions, raw HTTP on port 9000) mode needs no adapter and is out of scope.

Supported Frameworks

| Framework | Version | Status | | --------- | ------- | ------------ | | Express | 4.x | ✅ Supported | | Express | 5.x | ✅ Supported | | Koa | 2.x | ✅ Supported | | Koa | 3.x | ✅ Supported | | Hono | 4.x | ✅ Supported |

Note: Hono support requires Node.js >= 18 (for Web API Request/Response globals). Express and Koa continue to support Node.js >= 16.

Quick Start

Prerequisites

  • Node.js >= 16.x

Install

npm install @geek-fun/serverless-adapter

Usage

Auto-detect Provider (Recommended)

The adapter automatically detects the cloud provider based on the context object:

import express from 'express';
import serverlessAdapter from '@geek-fun/serverless-adapter';

const app = express();

app.get('/', (req, res) => {
  res.json({ message: 'Hello World!' });
});

// Auto-detect provider based on context
export const handler = serverlessAdapter(app);

Explicit Provider Selection

You can explicitly specify the provider:

import express from 'express';
import serverlessAdapter from '@geek-fun/serverless-adapter';

const app = express();

app.get('/', (req, res) => {
  res.json({ message: 'Hello from Tencent Cloud!' });
});

// Explicitly specify Tencent provider
export const main_handler = serverlessAdapter(app, { provider: 'tencent' });

Aliyun Function Compute Example

import express from 'express';
import serverlessAdapter from '@geek-fun/serverless-adapter';

const app = express();

app.get('/api/users', (req, res) => {
  res.json({ users: [] });
});

// Handler for Aliyun Function Compute
export const handler = serverlessAdapter(app);

Tencent SCF Example

import express from 'express';
import serverlessAdapter from '@geek-fun/serverless-adapter';

const app = express();

app.get('/api/users', (req, res) => {
  res.json({ users: [] });
});

// Handler for Tencent SCF
export const main_handler = serverlessAdapter(app, { provider: 'tencent' });

Volcengine veFaaS Example

import express from 'express';
import serverlessAdapter from '@geek-fun/serverless-adapter';

const app = express();

app.get('/api/users', (req, res) => {
  res.json({ users: [] });
});

// Handler for Volcengine veFaaS
export const handler = serverlessAdapter(app, { provider: 'volcengine' });

Cloudflare Workers Example

import express from 'express';
import serverlessAdapter from '@geek-fun/serverless-adapter';

const app = express();

app.get('/api/users', (req, res) => {
  res.json({ users: [] });
});

const handler = serverlessAdapter(app, { provider: 'cloudflare' });

export default {
  async fetch(request: Request, context: ExecutionContext): Promise<Response> {
    const result = await handler(request, context);
    const headers = new Headers(result.headers as Record<string, string>);
    (result.multiValueHeaders ?? {}).setCookie?.forEach((cookie) => headers.append('set-cookie', cookie));

    const body = result.isBase64Encoded
      ? Buffer.from(result.body, 'base64')
      : result.body;

    return new Response(body, { status: result.statusCode, headers });
  },
};

Hono Example

import { Hono } from 'hono';
import serverlessAdapter from '@geek-fun/serverless-adapter';

const app = new Hono();

app.get('/', (c) => c.json({ message: 'Hello from Hono!' }));

// Auto-detect provider based on context
export const handler = serverlessAdapter(app);

API Reference

serverlessAdapter(app, options?)

Creates a serverless handler for your Express, Koa, or Hono application. This positional form is the canonical shape for HTTP-only apps.

Parameters

| Parameter | Type | Required | Description | | -------------------------- | --------------------------------------- | -------- | ------------------------------------------------------------ | | app | Express \| Koa \| Hono | Yes | Express, Koa, or Hono application instance | | options.provider | 'aliyun' \| 'tencent' \| 'volcengine' \| 'aws' \| 'cloudflare' | No | Explicitly specify cloud provider (auto-detected if omitted) | | options.events | { timer?, nonHttp? } | No | Handlers for non-HTTP triggers, keyed by event kind — see Timer and non-HTTP triggers | | options.onUnhandledEvent | 'error' \| 'ignore' | No | What to do with a non-HTTP invocation that has no matching events handler. Defaults to 'error' (throw); 'ignore' restores the historical behaviour — see Timer and non-HTTP triggers |

Returns

A function that handles serverless events. Its result type follows the form:

// HTTP-only forms (positional app, no `events` handlers): the HTTP envelope
(event: Buffer, context: ProviderContext) =>
  Promise<{
    statusCode: number;
    body: string;
    headers: Record<string, string>;
    isBase64Encoded: boolean;
  }>;

// Any form with `events` handlers: the invocation may be a timer / queue event,
// so the result is whatever that handler returned — `ServerlessHandler<unknown>`
(event: Buffer, context: ProviderContext) => Promise<unknown>;

HTTP invocations return the envelope; the result of a timer / non-HTTP invocation is whatever the corresponding events handler returned, passed through verbatim. The types mirror that exactly: once events handlers are configured the handler is typed ServerlessHandler<unknown>, so the compiler does not pretend a timer result is an HTTP envelope. ServerlessHandler<Result> and ServerlessHandlerResult are exported for annotating your own code.

serverlessAdapter({ provider, events: { http, timer } }) (symmetric form)

For multi-trigger functions the HTTP app can be declared inside events, next to the other handlers. Both forms are accepted — the first argument is unambiguous to tell apart (an Express app is a function, Koa exposes .callback, Hono exposes .fetch, an options object has none of those):

// HTTP-only app — canonical, unchanged
serverlessAdapter(app, { provider: 'volcengine' });

// multi-trigger function (HTTP + timer) — uniform, recommended for this case
serverlessAdapter({
  provider: 'volcengine',
  events: { http: app, timer: runTimerJob },
});

The symmetric form takes the same provider / onUnhandledEvent options; the positional form stays canonical for HTTP-only apps.

Timer and non-HTTP triggers

On Aliyun FC, Tencent SCF, Volcengine veFaaS and AWS Lambda one function has one handler and every trigger type is delivered to it as a different event shape. A timer trigger, a queue event or an object-storage notification therefore reaches the same handler as an HTTP request — but it carries no request to build and no HTTP response contract to satisfy.

Dispatching such an invocation into the web framework answers a request nobody made, and the platform records the invocation as successful: a scheduled job then silently never runs. The adapter therefore classifies every invocation before it is normalized. With events handlers registered, non-HTTP invocations are routed to them; without a matching handler the invocation fails loudly:

const http = serverlessAdapter(app);

export const handler = (event, context) => http(event, context);
// A timer trigger makes this invocation reject with:
//   UnsupportedEventError: Unsupported timer invocation for provider "aliyun"
//   (trigger "billing-run"): this adapter only handles HTTP events. Handle
//   non-HTTP triggers in your own entrypoint, or pass
//   { onUnhandledEvent: 'ignore' } to keep the previous behaviour.
//   Event keys: triggerTime, triggerName, payload.

Handling timers with events.timer

Register an events.timer handler and the adapter normalizes every positively identified timer invocation into a provider-agnostic TimerEvent for you:

import serverlessAdapter from '@geek-fun/serverless-adapter';

const runTimerJob = async (event: TimerEvent) => {
  console.log(`timer ${event.triggerName} fired at ${event.triggerTime}`);
  if (event.payload?.job === 'billing-run') {
    await runBillingRun();
  }
  // the return value is passed through to the platform verbatim —
  // it is never wrapped into a fake statusCode/body
};

export const handler = serverlessAdapter(app, {
  provider: 'aliyun',
  events: {
    timer: runTimerJob,
    // anything positively identified as non-HTTP but not a timer
    // (queue events, object-storage notifications, …)
    nonHttp: (raw, context) => handleOtherTrigger(raw, context),
  },
});

The normalized envelope is identical across providers:

export type TimerEvent = {
  provider: CloudProvider; // informational (logs/telemetry), never a dispatch key
  triggerName?: string;
  triggerTime?: string;
  payload?: unknown;       // JSON-parsed when parseable, otherwise the raw string
  raw: unknown;            // the untouched platform event — never lose information
};

Or use the symmetric form, which reads the same as the deployment it describes:

export const handler = serverlessAdapter({
  provider: 'aliyun',
  events: { http: app, timer: runTimerJob },
});

Notes:

  • An error thrown inside events.timer / events.nonHttp propagates and fails the invocation — it is never converted into a 500 HTTP response.
  • onUnhandledEvent only governs invocations without a matching handler: 'error' (default) throws UnsupportedEventError; 'ignore' restores the historical permissive dispatch. An explicit handler always wins.

| Invocation | With matching events handler | Default ('error') | 'ignore' | | ------------------------------------------ | ------------------------------ | ------------------------------------------ | ----------------------------- | | HTTP event | handled by the framework | handled by the framework | handled by the framework | | Timer event | events.timer (normalized) | UnsupportedEventError — invocation fails | dispatched anyway (legacy) | | Recognized non-HTTP event of another kind | — | UnsupportedEventError — invocation fails | dispatched anyway (legacy) | | Unrecognized event | events.nonHttp (raw) | UnsupportedEventError — invocation fails | dispatched anyway (legacy) |

Failing the invocation matters: FC / SCF / Lambda only mark an invocation as failed when the handler throws, which is what puts it into logs, alerts and the platform's retry policy. onUnhandledEvent: 'ignore' exists only as a deprecation escape hatch for code that relied on the previous permissive behaviour.

What is recognized per provider:

| Provider | Recognized timer envelope | Notes | | ----------------- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | Aliyun FC3 | {triggerTime, triggerName, payload} | Delivered as a Buffer; plain objects (e.g. si local) are accepted as well | | Tencent SCF | {Type: 'Timer', TriggerName, Time, Message} | — | | Volcengine veFaaS | CloudEvents {id, source: '/faas/event/timer/{timer_id}', type: 'faas.timer.event', time, data} | triggerName is the timer id parsed from source (the event carries no trigger name), triggerTime is time and payload is data. The SCF-style envelope that si local emits is still accepted. TOS / MQ / TLS CloudEvents are unknown and reach events.nonHttp | | AWS | {version, id, 'detail-type': 'Scheduled Event', source: 'aws.events', resources: [rule ARN]} | The rule name from resources[0] becomes triggerName (id as fallback) and detail becomes payload. An EventBridge Scheduler invoking the function with a custom input is indistinguishable from a hand-written event; it is reported as unknown and reaches events.nonHttp | | Cloudflare | — | Cron Triggers are delivered to the Worker's separate scheduled() export, which this adapter never sees — see Cloudflare boundary |

What is recognized as an HTTP event, per provider:

| Provider | HTTP fields | | ----------------- | ---------------------------------------------------------------------------------------- | | Aliyun FC3 / AWS | API Gateway shapes: path / rawPath / httpMethod (v1), version: '2.0' + rawPath (v2), ALB (requestContext.elb + httpMethod) | | Tencent SCF | Function URL (httpMethod, path optional) and the legacy API Gateway shape (queryStringParameters / requestContext) | | Volcengine veFaaS | Documented API Gateway structure: path + httpMethod + queryStringParameters + pathParameters; the legacy method / query spelling is still accepted |

Cloudflare boundary

Cloudflare Workers separates the entrypoints at the runtime level: fetch and scheduled are distinct exports the adapter cannot intercept. Compose them in the Worker entrypoint instead:

import serverlessAdapter from '@geek-fun/serverless-adapter';

const handler = serverlessAdapter(app);

export default {
  fetch: (request, env, ctx) => handler(request, ctx),
  scheduled: (controller, env, ctx) => runTimerJob({ /* your own shape */ }),
};

Keeping your own entrypoint (manual recipe)

If you prefer to classify in your own entrypoint instead of registering events handlers, the normalization is exported for reuse:

import serverlessAdapter, { isTimerEvent, normalizeTimerEvent } from '@geek-fun/serverless-adapter';

const http = serverlessAdapter(app, { provider: 'aliyun' });

export const handler = async (event, context) => {
  // Pass the provider your deployment targets: without it, both helpers scan
  // every provider's markers and attribute the event to the first match.
  const timer = normalizeTimerEvent(event, 'aliyun');

  if (timer?.triggerName === 'billing-run') {
    return runBillingRun(timer);
  }

  if (isTimerEvent(event, 'aliyun')) {
    return someOtherTimer(event); // a timer, but not one this function handles
  }

  return http(event, context);
};

Classification is conservative: http only when the event positively matches a known HTTP shape, timer only for a positively identified timer envelope, everything else unknown. Never guess.

Provider Detection

The adapter automatically detects the cloud provider by examining the context object:

| Provider | Detection Fields | | ---------- | -------------------------------------------------------- | | Aliyun | service.name, tracing, logger, function.memory | | Tencent | tencentcloud_region, tencentcloud_appid, namespace | | Volcengine | requestId, region, function.memoryMb | | AWS | awsRequestId, invokedFunctionArn, functionName | | Cloudflare | web Request event + waitUntil on context |

License

Apache-2.0