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

ovenless

v0.1.2

Published

Type-safe RPC for AWS Serverless Framework v3. One Lambda, one router, Zod validation, OpenAPI docs, and a proxy client with full TypeScript inference—no code generation.

Downloads

475

Readme

Ovenless

Type-safe RPC for AWS Serverless Framework v3. One Lambda, one router, Zod validation, OpenAPI docs, and a proxy client with full TypeScript inference—no code generation.

npm version License: MIT Bun

Why Ovenless

| Need | Ovenless | | -------------------------------- | -------------------------------------------------------------------------------- | | End-to-end types without codegen | createClient<typeof router>() infers inputs/outputs from your Zod schemas | | Single Lambda, many routes | Catch-all HTTP API (/{proxy+}) routes to one handler—fewer cold starts | | Public API docs | GET /docs (Scalar) and GET /openapi.json generated from the same Zod schemas | | Serverless v3 deploy | ovenless build emits dist/handler.js + serverless.yml |

Requirements: Bun for CLI and local dev · Node.js 20+ Lambda runtime (default) · TypeScript 5+ · Zod 4+


Quick start

1. Create a project

mkdir my-api && cd my-api
bun init -y
bun add ovenless zod
bun add -d typescript @types/bun

2. Define the router

src/router.ts:

import { z } from "zod";
import { createRouter, mutation, query } from "ovenless";

const users = [
  { id: "1", name: "Alice", email: "[email protected]" },
  { id: "2", name: "Bob", email: "[email protected]" },
];

export const appRouter = createRouter({
  health: query({
    output: z.object({
      status: z.literal("ok"),
      timestamp: z.string(),
    }),
    handler: () => ({
      status: "ok" as const,
      timestamp: new Date().toISOString(),
    }),
  }),

  users: createRouter({
    getById: query({
      input: z.object({ id: z.string().min(1) }),
      output: z.object({
        id: z.string(),
        name: z.string(),
        email: z.string().email(),
      }),
      handler: ({ id }) => {
        const user = users.find((u) => u.id === id);
        if (!user) throw new Error(`User not found: ${id}`);
        return user;
      },
    }),

    create: mutation({
      input: z.object({
        name: z.string().min(1),
        email: z.string().email(),
      }),
      output: z.object({
        id: z.string(),
        name: z.string(),
        email: z.string().email(),
      }),
      handler: (input) => {
        const user = { id: String(users.length + 1), ...input };
        users.push(user);
        return user;
      },
    }),
  }),
});

export type AppRouter = typeof appRouter;

3. Add config

ovenless.config.ts:

import { defineConfig } from "ovenless";
import { appRouter } from "./src/router.ts";

export default defineConfig({
  router: appRouter,
  service: "my-api",
  title: "My API",
  version: "1.0.0",
  port: 3000,
  aws: {
    region: "us-east-1",
    runtime: "nodejs20.x",
    stage: "dev",
  },
});

4. Run locally

bunx ovenless start
# or with file watching:
bunx ovenless start --watch

Example startup output:

  ▲ Ovenless start  ·  development
  - Environments: .env, .env.development, .env.local
  - Variables: 4 from env files (+ OVENLESS_PROFILE)

  Ovenless development  →  http://localhost:3000

  API       http://localhost:3000/
  Docs      http://localhost:3000/docs
  OpenAPI   http://localhost:3000/openapi.json

5. Call procedures over HTTP

# Health (no input)
curl -s -X POST http://localhost:3000/health | jq

# Query with JSON body
curl -s -X POST http://localhost:3000/users/getById \
  -H "Content-Type: application/json" \
  -d '{"id":"1"}' | jq

# Query via GET + query string (values are strings — use z.coerce in schemas)
curl -s "http://localhost:3000/users/getById?id=2" | jq

# Mutation
curl -s -X POST http://localhost:3000/users/create \
  -H "Content-Type: application/json" \
  -d '{"name":"Charlie","email":"[email protected]"}' | jq

Paths accept slashes (/users/getById) or dots (/users.getById).


Type-safe client

Install the client entry (same package, separate export):

bun add ovenless

src/client.ts (frontend or monorepo app):

import { createClient, OvenlessClientError } from "ovenless/client";
import type { AppRouter } from "./router.ts";
import { appRouter } from "./router.ts";

export const api = createClient<AppRouter>({
  url: process.env.API_URL ?? "http://localhost:3000",
  router: appRouter, // optional: runtime path checks + GET for void-input queries
  headers: () => ({
    Authorization: `Bearer ${getToken()}`,
  }),
});

function getToken(): string {
  return "";
}

// Usage
async function main() {
  const health = await api.health();
  console.log(health.status); // "ok"

  const user = await api.users.getById({ id: "1" });
  console.log(user.name); // "Alice"

  const created = await api.users.create({
    name: "Dana",
    email: "[email protected]",
  });
  console.log(created.id);

  try {
    await api.users.getById({ id: "missing" });
  } catch (err) {
    if (err instanceof OvenlessClientError) {
      console.error(err.status, err.message, err.body);
    }
  }
}

Import only types on the frontend to avoid bundling server code:

import type { AppRouter } from "../api/src/router.ts";
import { createClient } from "ovenless/client";

const api = createClient<AppRouter>({
  url: import.meta.env.VITE_API_URL,
});

Optional: emit client types for publishing (tsconfig.client.json in your app):

bunx ovenless build:client

Procedures

Queries

Read-only operations. Allowed methods: GET or POST.

// No input
health: query({
  output: z.object({ ok: z.boolean() }),
  handler: () => ({ ok: true }),
}),

// With input
list: query({
  input: z.object({
    limit: z.coerce.number().optional().default(10),
  }),
  output: z.object({ items: z.array(z.string()) }),
  handler: ({ limit }) => ({ items: fetchItems(limit) }),
}),

For GET requests, input comes from the query string. All values are strings until Zod coerces them—use z.coerce.number(), z.coerce.boolean(), etc.

Mutations

Write operations. Allowed method: POST only.

create: mutation({
  input: z.object({ title: z.string() }),
  output: z.object({ id: z.string() }),
  handler: async (input) => {
    const id = await db.insert(input);
    return { id };
  },
}),

Nested routers

Group related procedures (maps to client.users.getById):

export const appRouter = createRouter({
  users: createRouter({
    getById: query({
      /* ... */
    }),
    create: mutation({
      /* ... */
    }),
  }),
});

Configuration

ovenless.config.ts must default-export the result of defineConfig():

| Field | Required | Description | | ------------- | -------- | --------------------------------------------------------------- | | router | yes | Router from createRouter() | | service | yes | Serverless service name | | title | no | OpenAPI title (defaults to service) | | version | no | OpenAPI version (default 0.1.0 in dev) | | port | no | Local dev port (default 3000, overridden by PORT) | | aws.region | no | AWS region (default us-east-1 or AWS_REGION) | | aws.runtime | no | Lambda runtime (default nodejs20.x) | | aws.stage | no | Deploy stage (default from profile: dev / staging / prod) |


Environment files

Loaded in order (later overrides earlier), Next.js-style logging on start and build:

| File | When | | ------------------------------------------------------- | ------------------------- | | .env | Always | | .env.development / .env.staging / .env.production | Matching --profile | | .env.local | Always (highest priority) |

# .env
LOG_LEVEL=info

# .env.development
API_URL=http://localhost:3000

# .env.local (gitignored)
DATABASE_URL=postgres://localhost:5432/dev

ovenless start defaults to profile development.
ovenless build defaults to profile production unless you pass --profile.

bunx ovenless start --profile staging
bunx ovenless build --profile staging

CLI

| Command | Description | | ------------------------------ | -------------------------------------------------- | | ovenless start | Local Bun HTTP server | | ovenless start --watch | Restart on file changes | | ovenless start --profile <p> | Load .env.<p> | | ovenless build --profile <p> | Bundle Lambda + write serverless.yml | | ovenless build:client | Emit client .d.ts (needs tsconfig.client.json) | | ovenless certs | Generate RSA PEM keys for JWT signing (RS256) | | ovenless help | Show usage |

JWT signing keys (ovenless certs)

Cross-platform (no ssh-keygen / openssl required). Writes keys under cert/<profile>/:

bunx ovenless certs --profile development
bunx ovenless certs --profile staging --comment "my-api staging"
bunx ovenless certs --profile production --force   # overwrite existing keys

Output layout:

cert/
├── development/
│   ├── id_rsa       # private key (PEM PKCS#1, mode 600)
│   └── id_rsa.pub   # public key (PEM)
├── staging/
└── production/

Use in your app (example with jose or jsonwebtoken):

import { readFileSync } from "node:fs";
import { join } from "node:path";

const profile = process.env.OVENLESS_PROFILE ?? "development";
const privateKey = readFileSync(join("cert", profile, "id_rsa"), "utf8");
const publicKey = readFileSync(join("cert", profile, "id_rsa.pub"), "utf8");

Add cert/ to .gitignore.


JWT authentication (built-in)

Enable auth on the router (not per-procedure). All routes are protected unless marked meta: { public: true } or listed in auth.public.

bunx ovenless certs --profile development
import { z } from "zod";
import { createRouter, mutation, query } from "ovenless";

const claimsSchema = z.object({ role: z.enum(["user", "admin"]) });

export const appRouter = createRouter(
  {
    health: query({
      meta: { public: true },
      output: z.object({ ok: z.boolean() }),
      handler: () => ({ ok: true }),
    }),

    login: mutation({
      meta: { public: true },
      input: z.object({ userId: z.string() }),
      output: z.object({ ok: z.literal(true) }),
      handler: async ({ userId, auth }) => {
        await auth.sign({
          principalId: userId,
          claims: { role: "user" },
        });
        return { ok: true };
      },
    }),

    me: query({
      output: z.object({ principalId: z.string(), role: z.string() }),
      handler: ({ principalId, claims }) => ({
        principalId,
        role: claims.role,
      }),
    }),
  },
  {
    auth: {
      mode: "bearer", // or "cookie"
      ttl: "7d", // "10m", "1h", or seconds
      claims: claimsSchema,
      autoRotate: true, // cookie mode: refresh Set-Cookie when near expiry
      public: ["health", "login"],
      authorizer: true, // generate Lambda REQUEST authorizer on build
      cookie: { name: "ovenless_token", secure: true, sameSite: "lax" },
    },
  },
);

Handler context

When auth is enabled, handlers receive one merged object: validated input fields (spread), plus input (the full parsed body), plus auth fields.

// Destructure input fields and auth together
handler: ({ limit, principalId, claims }) => { ... }

// Or use the nested `input` alias
handler: ({ input, principalId }) => createUser(input)

| Field | Description | |-------|-------------| | …input fields | Spread from the procedure input schema | | input | Full parsed input object (same fields as spread) | | principalId | JWT sub (Serverless principalId compatible) | | claims | Typed from auth.claims Zod schema | | auth.sign | Issue JWT (Set-Cookie automatically in cookie mode) | | auth.setCookie / auth.clearCookie | Cookie helpers |

Client

import { createClient, setClientBearerToken } from "ovenless/client";

const api = createClient<AppRouter>({
  url: "http://localhost:3000",
  auth: { mode: "bearer" },
});

await api.login({ userId: "alice" });
// After login, set token from your app logic or use cookie mode:
const apiCookie = createClient<AppRouter>({
  url: "http://localhost:3000",
  auth: { mode: "cookie" },
});
setClientBearerToken("eyJhbG...");
await api.me();

Lambda authorizer

When auth.authorizer: true, ovenless build emits dist/authorizer.js and wires HTTP API ovenlessJwt in serverless.yml. The main handler still validates JWT in-process; API Gateway can reject invalid tokens earlier.

import { createJwtAuthorizer } from "ovenless";

export const jwtAuthorizer = createJwtAuthorizer({ auth: appRouter.auth! });

Deploy to AWS

1. Build for the target profile

bunx ovenless build --profile production

Outputs:

  • dist/handler.js — CommonJS bundle, export awsHandler
  • serverless.yml — generated Serverless Framework v3 config

Generated serverless.yml (excerpt):

service: my-api
frameworkVersion: "3"
provider:
  name: aws
  runtime: nodejs20.x
  region: us-east-1
  stage: prod
functions:
  api:
    handler: dist/handler.awsHandler
    events:
      - httpApi:
          path: /{proxy+}
          method: ANY
      - httpApi:
          path: /
          method: ANY

2. Deploy with Serverless

npm i -g serverless
serverless deploy --stage prod

3. Custom Lambda entry (advanced)

If you hand-roll the handler instead of ovenless build:

import { createAwsHandler } from "ovenless";
import config from "./ovenless.config.ts";

export const handler = createAwsHandler(config.router, {
  title: config.title ?? config.service,
  version: config.version ?? "1.0.0",
});

HTTP API

Built-in routes

| Method | Path | Description | | --------- | --------------- | -------------------------------- | | GET | / | API metadata + procedure list | | GET | /docs | Scalar interactive documentation | | GET | /openapi.json | OpenAPI 3.0 document | | OPTIONS | * | CORS preflight |

Procedure routes

| Type | Methods | Body | | -------- | ------------- | ------------------------------------- | | Query | GET, POST | GET: query string · POST: JSON object | | Mutation | POST | JSON object |

Success response

JSON body = validated procedure output schema.

Error response

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Input validation failed",
    "details": []
  }
}

| Code | HTTP | Meaning | | -------------------- | ---- | ----------------------------------------- | | NOT_FOUND | 404 | Unknown procedure path | | METHOD_NOT_ALLOWED | 405 | Wrong HTTP method | | INVALID_BODY | 400 | Body not a JSON object | | INVALID_JSON | 400 | Malformed JSON (dev server) | | VALIDATION_ERROR | 400 | Zod input/output validation failed | | INTERNAL_ERROR | 500 | Handler threw (message hidden by default) |

Production 500 responses use a generic message. Enable details only in development:

createHandler(router, {
  exposeErrorDetails: process.env.NODE_ENV !== "production",
});

Architecture

┌──────────────┐     types only      ┌─────────────────┐
│   Frontend   │ ◄────────────────── │  appRouter      │
│ createClient │      fetch/POST     │  (Zod + TS)     │
└──────┬───────┘                     └────────┬────────┘
       │                                        │
       │  POST /users.getById                   │ createHandler
       ▼                                        ▼
┌──────────────────────────────────────────────────────────┐
│  Bun dev server  ·  or  AWS API Gateway HTTP API         │
│                     └─► Lambda (awsHandler)              │
│                           ├─ Zod input parse             │
│                           ├─ procedure.handler()         │
│                           ├─ Zod output parse            │
│                           └─ /docs · /openapi.json       │
└──────────────────────────────────────────────────────────┘

Package exports

| Import | Use | | ----------------- | ------------------------------------------------------------ | | ovenless | Router, handler, AWS adapter, config, CLI | | ovenless/client | createClient, client types (InferClient, AppClient, …) |


Programmatic usage

Standalone HTTP handler (tests, custom servers):

import { createHandler } from "ovenless";
import { appRouter } from "./src/router.ts";

const handle = createHandler(appRouter, {
  title: "My API",
  version: "1.0.0",
  cors: true,
});

const res = await handle({
  method: "POST",
  path: "/health",
  body: {},
});

console.log(res.statusCode, res.body);

Project layout (reference app)

my-api/
├── ovenless.config.ts    # defineConfig({ router, service, ... })
├── serverless.yml        # generated by ovenless build
├── package.json
├── .env
├── .env.development
├── .env.local
├── src/
│   └── router.ts         # appRouter + export type AppRouter
└── dist/
    └── handler.js        # Lambda bundle (after build)

Publishing (GitHub Actions + npm Trusted Publishing)

Releases are published from CI using npm Trusted Publishing (OIDC). No long-lived NPM_TOKEN is stored in GitHub Secrets.

One-time: configure Trusted Publisher on npm

  1. Sign in at npmjs.com → open the ovenless package → SettingsTrusted Publisher (or Publishing access → add trusted publisher).

  2. Choose GitHub Actions and enter:

    | Field | Value | |-------|--------| | Organization or user | gbmungunshagai-png | | Repository | ovenless | | Workflow filename | publish.yml | | Environment | (leave empty unless you add a GitHub Environment) |

  3. Save. Fields are case-sensitive and must match the workflow file name exactly.

Ensure the package is not set to “Require two-factor authentication and disallow tokens” in a way that blocks CI — Trusted Publishing uses OIDC, not granular tokens.

Release flow

  1. Bump version in package.json (e.g. 0.1.20.1.3).

  2. Commit and push to main.

  3. Tag and push (tag must match package.json version):

    git tag v0.1.3
    git push origin v0.1.3

    The Publish workflow runs tests, builds via prepublishOnly, and runs npm publish.

  4. Or trigger manually: ActionsPublishRun workflow.

CI on every push/PR is handled by .github/workflows/ci.yml.


Scripts (developing Ovenless itself)

bun run build      # bundle dist/
bun test           # run test suite
bun run typecheck  # tsc --noEmit

Roadmap

  • [ ] Middleware and request context (auth, logging)
  • [ ] Subscription / streaming procedures
  • [ ] First-party project scaffold (create-ovenless)

License

MIT © G.B.Mungunshagai