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

upstash-lua

v0.3.1

Published

Type-safe Lua scripts for Upstash with StandardSchema validation

Readme

Hero

Type-safe Lua scripts for Upstash Redis with StandardSchemaV1 validation.

Disclaimer: This library is not affiliated with or officially supported by Upstash. The name "upstash-lua" reflects that it's designed to work with Upstash Redis and provides utilities for writing Lua scripts.

Features

  • Full TypeScript inference for keys, args, and return values
  • Type-safe Lua templates - Use ${KEYS.name} and ${ARGV.name} with autocomplete and compile-time errors
  • Input validation using StandardSchemaV1 schemas (Zod, Effect Schema, ArkType, etc.)
  • Efficient execution via EVALSHA with automatic NOSCRIPT fallback
  • Universal runtime support - Node.js 18+, Bun, Cloudflare Workers, Vercel Edge

Installation

bun add upstash-lua @upstash/redis
# or
pnpm install upstash-lua @upstash/redis

Quick Start

import { z } from "zod"
import { defineScript, lua } from "upstash-lua"
import { Redis } from "@upstash/redis"

const redis = new Redis({
  url: process.env.UPSTASH_REDIS_REST_URL,
  token: process.env.UPSTASH_REDIS_REST_TOKEN,
})

// Define a rate limiter script with type-safe Lua template
const rateLimit = defineScript({
  name: "rateLimit",
  keys: {
    key: z.string(),
  },
  args: {
    limit: z.number().int().positive().transform(String),
    windowSeconds: z.number().int().positive().transform(String),
  },
  lua: ({ KEYS, ARGV }) => lua`
    local current = redis.call("INCR", ${KEYS.key})
    if current == 1 then
      redis.call("EXPIRE", ${KEYS.key}, ${ARGV.windowSeconds})
    end
    local allowed = current <= tonumber(${ARGV.limit}) and 1 or 0
    return { allowed, tonumber(${ARGV.limit}) - current }
  `,
  returns: z.tuple([z.number(), z.number()]).transform(([allowed, rem]) => ({
    allowed: allowed === 1,
    remaining: rem,
  })),
})

// Execute with full type safety
const result = await rateLimit.run(redis, {
  keys: { key: "rl:user:123" },
  args: { limit: 10, windowSeconds: 60 },
})

console.log(result.allowed)   // boolean
console.log(result.remaining) // number

API

defineScript(options)

Creates a type-safe Lua script definition.

Options

| Property | Type | Description | |----------|------|-------------| | name | string | Human-readable name (used in error messages) | | lua | string \| LuaFunction | Lua script source code (string) or type-safe template function | | keys | Record<string, Schema> | Key schemas - order determines KEYS[1], KEYS[2], etc. | | args | Record<string, Schema> | Arg schemas - order determines ARGV[1], ARGV[2], etc. | | returns | Schema | Optional return value schema |

Important: Key/arg order is determined by object literal insertion order. Always define using object literal syntax in the intended order.

Lua Property: String vs Function

The lua property can be either a plain string or a function that returns a lua template:

String form (manual KEYS/ARGV indexing):

lua: `return redis.call("GET", KEYS[1])`

Function form (type-safe with autocomplete):

lua: ({ KEYS, ARGV }) => lua`
  return redis.call("GET", ${KEYS.userKey})
`

The function form provides:

  • ✅ Autocomplete for KEYS.* and ARGV.* properties
  • ✅ Compile-time errors for invalid key/arg references
  • ✅ Automatic compilation of ${KEYS.name}KEYS[n] and ${ARGV.name}ARGV[n]

Returns

A Script object with:

  • run(redis, input) - Execute with full validation
  • runRaw(redis, input) - Execute without return validation
  • name, lua, keyNames, argNames - Metadata

Schema Requirements

Keys and args must output strings (Redis only accepts strings). Use transforms:

args: {
  // Number input → string output
  limit: z.number().transform(String),
  
  // Boolean input → "1" or "0" output
  enabled: z.boolean().transform(b => b ? "1" : "0"),
  
  // String input → string output (no transform needed)
  key: z.string(),
}

Examples

Type-Safe Lua Template

Use the lua tagged template function for type-safe key and argument references:

import { defineScript, lua } from "upstash-lua"
import { z } from "zod"

const getUser = defineScript({
  name: "getUser",
  keys: {
    userKey: z.string(),
  },
  args: {
    field: z.string(),
  },
  lua: ({ KEYS, ARGV }) => lua`
    -- TypeScript autocomplete works here!
    local key = ${KEYS.userKey}
    local field = ${ARGV.field}
    return redis.call("HGET", key, field)
  `,
  returns: z.string().nullable(),
})

// ${KEYS.userKey} automatically becomes KEYS[1]
// ${ARGV.field} automatically becomes ARGV[1]

Simple Script (No Keys/Args)

const ping = defineScript({
  name: "ping",
  lua: 'return redis.call("PING")',
  returns: z.string(),
})

const result = await ping.run(redis)
// result: "PONG"

Or with the function form:

const ping = defineScript({
  name: "ping",
  lua: () => lua`return redis.call("PING")`,
  returns: z.string(),
})

Script Without Return Validation

const getData = defineScript({
  name: "getData",
  lua: 'return redis.call("HGETALL", KEYS[1])',
  keys: { key: z.string() },
  // No returns schema - result is unknown
})

const result = await getData.run(redis, { keys: { key: "user:123" } })
// result: unknown

HGETALL with hashResult()

Redis commands like HGETALL return flat arrays of alternating key-value pairs: ["field1", "value1", "field2", "value2"]

The hashResult() helper converts this to an object before validation, enabling you to use z.object():

import { z } from "zod"
import { defineScript, hashResult } from "upstash-lua"

const getUser = defineScript({
  name: "getUser",
  keys: { key: z.string() },
  lua: 'return redis.call("HGETALL", KEYS[1])',
  returns: hashResult(z.object({
    name: z.string(),
    email: z.string(),
    age: z.coerce.number(),
    is_admin: z.string().transform(v => v === "true"),
  })),
})

const user = await getUser.run(redis, { keys: { key: "user:123" } })
// user: { name: string, email: string, age: number, is_admin: boolean }

Works with all Zod object features:

// Optional fields
returns: hashResult(z.object({
  name: z.string(),
  email: z.string().optional(),
}))

// Partial objects
returns: hashResult(z.object({
  name: z.string(),
  email: z.string(),
}).partial())

// Passthrough for extra fields
returns: hashResult(z.object({
  name: z.string(),
}).passthrough())

Effect Schema Example

import { Schema } from "effect"
import { defineScript, lua } from "upstash-lua"

const incr = defineScript({
  name: "incr",
  keys: {
    key: Schema.standardSchemaV1(Schema.String),
  },
  args: {
    amount: Schema.standardSchemaV1(
      Schema.Number.pipe(
        Schema.transform(Schema.String, (n) => String(n), (s) => Number(s))
      )
    ),
  },
  lua: ({ KEYS, ARGV }) => lua`
    return redis.call("INCRBY", ${KEYS.key}, ${ARGV.amount})
  `,
  returns: Schema.standardSchemaV1(Schema.Number),
})

Error Handling

Errors are thrown as Error objects with descriptive messages when validation fails:

try {
  await script.run(redis, { args: { limit: -1 } })
} catch (error) {
  if (error instanceof Error) {
    console.error(error.message)
    // "[[email protected]] Script \"rateLimit\" input validation failed at \"args.limit\": Number must be positive"
  }
}

Error messages include:

  • The library version
  • The script name
  • The validation path (for input errors)
  • The validation error messages

How It Works

  1. Define - Creates script with metadata and computed SHA1
    • If lua is a function, it's called with typed KEYS/ARGV proxies
    • The lua template is compiled: ${KEYS.name}KEYS[n], ${ARGV.name}ARGV[n]
  2. Validate - Keys/args are validated against StandardSchemaV1 schemas
  3. Transform - Validated values are transformed (e.g., numbers → strings)
  4. Execute - Uses EVALSHA for efficiency, falls back to SCRIPT LOAD on NOSCRIPT
  5. Parse - Return value is validated/transformed if schema provided

The library caches script loading per-client, so concurrent calls share a single SCRIPT LOAD.

Versioning

import { VERSION } from "upstash-lua"
console.log(`Using upstash-lua v${VERSION}`)

Errors include the version for debugging:

[[email protected]] Script "rateLimit" input validation failed at "args.limit": ...

License

MIT