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

@kirkelliott/zap

v0.1.30

Published

Drop a .zap file in S3. It becomes an API endpoint.

Readme

zap

Drop a .zap file in S3. It becomes an API endpoint.

Drop hello.zap → GET /hello returns a response. Change the file → behavior changes instantly. No redeploy. No CI. No config.


Setup

mkdir my-project && cd my-project
npx @kirkelliott/zap init

Provisions everything on your AWS account and prints a URL. Takes about 30 seconds. Requires AWS credentials — run aws configure first if you haven't.

  packaging runtime       ✓
  creating bucket         ✓  zap-a3f2b8c1
  creating kv table       ✓  zap-kv
  configuring iam         ✓
  deploying lambda        ✓
  creating endpoint       ✓

  → https://abc123.lambda-url.us-east-1.on.aws

Deploy a handler

npx @kirkelliott/zap deploy hello.zap

Or install the CLI once and drop the npx:

npm install -g @kirkelliott/zap
zap deploy hello.zap

The .zap format

A .zap file exports one function. The runtime rewrites export default to module.exports and runs it in a Node.js vm context — isolated from the Lambda environment, with only the globals listed below in scope.

export default async (req) => {
  return { body: `hello ${req.query.name ?? 'world'}` }
}

Request

req.method   // 'GET' | 'POST' | ...
req.path     // '/hello'
req.params   // ['123'] — path segments beyond the matched handler
req.query    // { name: 'elliott' }
req.headers  // { 'content-type': '...' }
req.body     // string | null

Response

{
  status?:  number                   // default 200
  headers?: Record<string, string>
  body?:    string | object          // objects become JSON automatically
}

Routing

The filename is the route. hello.zap answers /hello. index.zap answers /.

If no exact file matches, the runtime walks up the path until it finds one:

GET /users/123        →  users/123.zap  (exact)
                      →  users.zap      (fallback)
                      →  404

The handler always receives the full original path in req.path, so it can parse its own segments:

// users.zap — handles /users/:id
export default async (req) => {
  const [id] = req.params
  return { body: { id } }
}

Globals

These are the only names in scope inside a .zap file.

Injected by zap:

| Name | What it does | |---|---| | kv | Persistent key/value storage (DynamoDB-backed) | | zap(name) | Load another .zap from the same S3 bucket |

Standard Node.js 20 globals passed through:

| Name | What it does | |---|---| | fetch | HTTP requests | | crypto | Web Crypto API | | URL, URLSearchParams | URL parsing | | Buffer | Binary data | | setTimeout, clearTimeout | Timers | | console | Logs to CloudWatch | | process.env | Environment variables (process is not otherwise available) |

require, the file system, and the outer Lambda scope are not accessible.


kv — storage that persists

await kv.set('key', value)   // string, number, object, array — anything
await kv.get('key')          // returns the value, or null
await kv.del('key')          // delete
// counter.zap
export default async (req) => {
  const count = ((await kv.get('visits')) ?? 0) + 1
  await kv.set('visits', count)
  return { body: { visits: count } }
}

No database to provision. It's just there.


zap() — one handler can load another

// utils/auth.zap
export default {
  verify: (token) => token === process.env.SECRET,
}
// api.zap
export default async (req) => {
  const auth = await zap('utils/auth')
  if (!auth.verify(req.headers.authorization))
    return { status: 401, body: 'unauthorized' }
  return { body: 'ok' }
}

S3 is the module system. Drop a file in, import it anywhere.


@cron — run on a schedule

First line // @cron <expr> turns any handler into a scheduled job. zap deploy wires up the EventBridge rule automatically.

// @cron 0 * * * *
export default async () => {
  await kv.set('heartbeat', new Date().toISOString())
}

Standard cron syntax: minute hour day month weekday. zap rm removes the schedule when you remove the file.


Examples

CORS proxy

// proxy.zap
export default async (req) => {
  const url = req.query.url
  if (!url) return { status: 400, body: 'Missing ?url=' }

  const upstream = await fetch(url)
  return {
    status: upstream.status,
    headers: {
      'content-type': upstream.headers.get('content-type') ?? 'text/plain',
      'access-control-allow-origin': '*',
    },
    body: await upstream.text(),
  }
}
GET https://your-endpoint/proxy?url=https://api.example.com/data

Shared layout — imports in action

// lib/page.zap — shared layout
export default (title, content) => ({
  headers: { 'content-type': 'text/html' },
  body: `<!doctype html><html>...${title}...${content}...</html>`,
})
// iss.zap — ISS position, live
export default async (req) => {
  const page = await zap('lib/page')
  const { iss_position } = await (await fetch('http://api.open-notify.org/iss-now.json')).json()
  return page('ISS', `${iss_position.latitude} / ${iss_position.longitude}`)
}
// astros.zap — who's in space right now
export default async (req) => {
  const page = await zap('lib/page')
  const { people } = await (await fetch('http://api.open-notify.org/astros.json')).json()
  return page('In space', people.map(p => p.name).join(', '))
}

Both pages share lib/page.zap. Update that one file in S3 — both pages change instantly. S3 is the module system.


Environments

Each environment is a fully isolated AWS stack — its own S3 bucket, Lambda function, KV table, and URL. prod is the default. No flags needed for prod.

# spin up staging (takes ~30 seconds, same as init)
zap init --env staging

# deploy and test there
zap deploy --env staging api.zap
curl https://staging-url.lambda-url.us-east-1.on.aws/api

# promote to prod when ready
zap promote api --from staging --to prod

promote copies the file from one bucket to the other. The prod URL is live instantly.

.zaprc stores each environment under its own key:

{
  "prod":    { "bucket": "zap-a3f2b8c1", "url": "https://abc.lambda-url…", … },
  "staging": { "bucket": "zap-f9e2d4c7", "url": "https://xyz.lambda-url…", … }
}

Every command accepts --env:

zap ls --env staging
zap rollback api --env staging
zap rm old-handler --env staging

rollback — undo a deploy

Every zap deploy creates a new S3 version. Roll back to the previous one instantly:

zap rollback hello
# ↩  hello  restored to 2026-02-28T19:35:00.000Z

Works per-environment: zap rollback hello --env staging

No revert commits. No redeploy. The old code is live immediately.


Observability

Every request emits a structured JSON log line to CloudWatch:

{"handler":"hello","method":"GET","status":200,"ms":43,"requestId":"abc-123"}

Every cache refresh (S3 fetch) logs the handler, ETag, and last-modified timestamp:

{"cache":"refresh","handler":"hello","etag":"\"d41d8cd9\"","modified":"2026-02-28T19:35:00.000Z"}

The requestId correlates with Lambda's own REPORT line, which adds total duration, billed duration, and memory. Nothing to configure — it's all in CloudWatch Logs.

Query with CloudWatch Logs Insights:

filter ispresent(handler)
| stats avg(ms), count() as reqs by handler
| sort avg(ms) desc

CLI

zap init [--env <env>]              Provision AWS and deploy the runtime
zap deploy <file|dir> [--env <env>] Upload .zap file(s) to S3
zap promote <name> [--from] [--to]  Copy a handler between environments
zap rollback <name> [--env <env>]   Restore the previous version of a handler
zap rm <name> [--env <env>]         Remove a handler (and its cron rule)
zap ls [--env <env>]                List deployed handlers
zap demo [--env <env>]              Deploy the built-in demo handlers
zap repair [--env <env>]            Fix Lambda permissions if the URL stops working

init writes a .zaprc to the project directory. All other commands read bucket and region from it — no flags needed.


What gets created

zap init provisions six things on your AWS account:

| Resource | What it is | |---|---| | S3 bucket | Where your .zap files live | | DynamoDB table | Backs kv | | IAM role | Lets Lambda read S3 and write DynamoDB | | Lambda function | The runtime that runs your handlers | | Lambda Function URL | Your public HTTPS endpoint | | EventBridge rules | One per @cron handler |


Cost

All within the AWS permanent free tier:

| Service | Free tier | |---|---| | Lambda | 1M requests/month, 400K GB-seconds | | S3 | 5GB, 20K GET requests/month | | DynamoDB | 25 WCU/RCU, 25GB | | EventBridge | 14M scheduled invocations/month |


How it works

One Lambda function (Node.js 20) runs permanently. Every request:

  1. Parses the path — /hello → fetches hello.zap from S3
  2. Rewrites export default to module.exports
  3. Runs the code in a vm.runInNewContext sandbox with the globals listed above
  4. Calls the exported function with the request
  5. Returns the response

Source is cached in Lambda memory for 5 seconds. Deploys propagate within 5 seconds on warm containers.


MIT


live demo →