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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@safekit/safe-fn

v0.7.1

Published

A lightweight type-safe function builder with interceptors, schema validation, and context management for TypeScript applications.

Readme

@safekit/safe-fn

⚠️ EXPERIMENTAL ALPHA VERSION ⚠️ This package is an experimental package in active development and not ready for production use. Expect breaking changes between versions.

A lightweight type-safe function builder with interceptors, schema validation, and context management for TypeScript applications.

npm version TypeScript License: MIT

Features

  • 🔒 Type-Safe: Full TypeScript support with automatic type inference and structural typing
  • 🔗 Middleware: Chainable middleware for cross-cutting concerns with proper context evolution
  • StandardSchema Support: Works with StandardSchema V1 compatible validators including Zod, Valibot, ArkType, and Effect Schema
  • 🎨 Context Management: Type-safe context passing and binding with .context<T>() and .withContext()
  • 🚀 Lightweight: Zero runtime dependencies

Installation

# npm
npm install @safekit/safe-fn

# yarn
yarn add @safekit/safe-fn

# bun
bun add @safekit/safe-fn

Quick Start

Here's a simple example showing how to create a client and define a function:

import { z } from "zod";

import { createClient } from "@safekit/safe-fn";

// 1. Create client
const client = createClient();

// 2. Define function
export const createUser = client
  .input(z.object({ username: z.string() }))
  .output(z.object({ id: z.string() }))
  .handler(async ({ ctx, input }) => {
    // ...
  });

Client Configuration

Clients are created with createClient() and support two main configuration options:

  • metadataSchema - schema for metadata
  • onError - error handler

Context is provided through middleware for maximum flexibility:

import { z } from "zod";

import { createClient } from "@safekit/safe-fn";

// 1. Create client with configuration
const client = createClient({
  metadataSchema: z.object({ traceId: z.string() }),
  onError: ({ error, ctx }) => {
    (ctx as any).logger?.error(`Error: ${error.message}`);
    // Return void to let error pass through
  },
})
// 2. Add context through middleware
.use(async ({ next }) => {
  return next({ ctx: { logger: console } });
});

Middleware

Middleware functions run before your handler and can modify context, validate permissions, log requests, and more. Create specialized clients by chaining .use():

import { z } from "zod";
import { createClient } from "@safekit/safe-fn";

// Base client with logging
const publicClient = createClient()
  .use(async ({ next }) => {
    // Base context middleware
    return next({ ctx: { logger: console } });
  })
  .use(async ({ ctx, metadata, next }) => {
    // Database middleware
    const db = getDbConnection();
    return next({ ctx: { db } });
  })
  .use(async ({ ctx, metadata, next }) => {
    // Logger middleware
    ctx.logger.info(metadata, `Attempting to ${metadata.operation}`);
    return next();
  });

// Authenticated client
const authedClient = publicClient.use(async ({ ctx, metadata, next }) => {
  const sessionToken = getCookie("sessionToken");
  const { user, session } = await getUserAndSession(ctx, sessionToken);
  return next({ ctx: { ...ctx, user, session } });
});

// Usage examples
export const publicFunction = publicClient
  .input(z.object({ query: z.string() }))
  .handler(async ({ input }) => {
    // ...
  });

export const protectedFunction = authedClient
  .metadata({ requiresAuth: true })
  .input(z.object({ postId: z.string() }))
  .handler(async ({ input, ctx }) => {
    // ...
  });

// Function calls
const search = await publicFunction({ query: "hello" });

const post = await protectedFunction({ postId: "123" });

Validation

safe-fn supports multiple popular validation libraries, giving you flexibility to use your preferred validation approach. All libraries support both object schemas and tuple schemas (multiple arguments):

import { z } from "zod";

import { createClient } from "@safekit/safe-fn";

const client = createClient();

// Object schemas - use `input` parameter
const zodObjectFn = client
  .input(z.object({ name: z.string(), age: z.number() }))
  .handler(async ({ input }) => {
    const { name, age } = input;
    // ...
  });

// Tuple schemas - use `args` parameter
const zodTupleFn = client
  .args(z.string(), z.number(), z.boolean()) // name, age, active
  .handler(async ({ args }) => {
    const [name, age, active] = args;
    // ...
  });

// Usage examples
const greeting = await zodObjectFn({ name: "Alice", age: 30 });
const person = await zodTupleFn("Bob", 25, true);

Error Handling

Safe-fn provides flexible error handling through the onError handler configured on the client. The error handler receives the same parameters as middleware functions: { error, ctx, metadata, rawInput, rawArgs, valid }.

Basic Error Handling

const client = createClient({
  onError: ({ error, ctx, metadata, rawInput, valid }) => {
    // Log error with full context
    console.error(`[${(ctx as any).service}] Error for user ${(ctx as any).userId}:`, error.message);
    console.log("Metadata:", metadata);
    console.log("Raw input:", rawInput);

    // Access validated input if schema exists
    try {
      const validInput = valid("input");
      console.log("Validated:", validInput);
    } catch {
      // No schema available
    }

    // Return void to let error pass through (default behavior)
  },
})
.use(async ({ next }) => {
  // Provide context through middleware
  return next({ ctx: { userId: "user123", service: "api" } });
});

Advanced Error Handling

The onError handler supports error recovery, transformation, and comprehensive logging:

const client = createClient({
  onError: ({ error, ctx, metadata, rawInput, valid }) => {
    // Log error with full context
    console.error(`[${(ctx as any).service}] Error for user ${(ctx as any).userId}:`, error.message);

    // Access validated input if available
    try {
      const validInput = valid("input");
      console.log("Validated input:", validInput);
    } catch {
      console.log("Raw input:", rawInput);
    }

    // Error recovery for specific cases
    if (error.message.includes("recoverable")) {
      return {
        success: true,
        data: `Recovered: ${error.message}`,
      };
    }

    // Error transformation
    if (error.message.includes("payment")) {
      return new Error(`[PAYMENT_ERROR] ${error.message} (${metadata.operation || "unknown"})`);
    }

    // Let other errors pass through
  },
})
.use(async ({ next }) => {
  // Provide context through middleware
  return next({ ctx: { userId: "user123", service: "api" } });
});

Return Types

The onError handler can return:

  • void - Error passes through unchanged (default)
  • Error - Replace with new error
  • { success: true, data: any } - Recover with data
  • { success: false, error: Error } - Replace with new error

See examples/error.examples.ts for a complete working example.

API Reference

createClient(config?)

Creates a new SafeFn client with optional configuration. See the Client Configuration section above for detailed examples.

Client Methods

  • .metadata(data) - Set metadata
  • .use(middleware) - Add middleware
  • .context<T>() - Set context type
  • .input(schema) - Set input validation
  • .args(schema) - Set tuple input validation
  • .output(schema) - Set output validation
  • .handler(fn) - Define the function

Context API

The context API enables type-safe context binding at call-time. Define context types with .context<T>() and bind values with .withContext():

import { z } from "zod";
import { createClient } from "@safekit/safe-fn";

type AuthContext = {
  db: Database;
  logger: Console;
};
const ctx = { db: new Database(), logger: console };

// Enable context capabilities
const fn = createClient()
  .context<AuthContext>()
  .handler(async ({ ctx }) => {
    const user = await ctx.db.getUser("user-123");
    ctx.logger.log(user);
    return user;
  });

// Bind context at call-time
const result = await fn.withContext(ctx).execute();

Context works with middleware - middleware receives the working context (base + input context):

const client = createClient()
  .context<AuthContext>()
  .use(async ({ ctx, next }) => {
    // ctx is properly typed as AuthContext
    ctx.logger.log("hi");
    return next();
  });

See examples/context.example.ts for complete examples.

Input Variants

safe-fn supports three ways to handle input:

  • <omitted> - No input.
  • .input<T>() - Typed input WITHOUT runtime validation.
  • .input(schema) - Typed input WITH runtime validation.
// No input - just call the function
client.handler(() => {});

// Type-only input - typed but no validation
client.input<{ name: string }>().handler(({ input }) => input.name);

// Validated input - schema validation + types
client.input(z.object({ name: z.string() })).handler(({ input }) => input.name);

Output Variants

safe-fn supports three ways to handle output:

  • <omitted> - No output.
  • .output(schema) - Typed output WITH runtime validation.
  • .output<T>() - Typed output WITHOUT runtime validation.
// No output - just call the function
client.handler(() => {});

// Type-only output - typed but no validation
client.output<{ name: string }>().handler(() => ({ name: "John" }));
client.output<void>().handler(() => {}); // void output

// Validated output - schema validation + types
client.output(z.object({ name: z.string() })).handler(() => ({ name: "John" }));
client.output(z.void()).handler(() => {}); // void output

Middleware Type

type Middleware<TContext> = (params: {
  next: NextFunction<TContext>;
  rawInput: unknown;
  rawArgs: unknown;
  ctx: TContext;
  metadata: Metadata;
  valid(type: "input"): any; // Get validated input data
  valid(type: "args"): any; // Get validated args data
}) => Promise<MiddlewareOutput>;

Accessing Validated Data in Middleware

Middleware can access both raw and validated data:

const middleware = createMiddleware(async ({ rawInput, rawArgs, valid, next }) => {
  // Raw data is always available
  console.log("Raw input:", rawInput);
  console.log("Raw args:", rawArgs);

  try {
    // Get validated data if schema exists
    const validInput = valid("input");
    console.log("✅ Validated input:", validInput);
  } catch (error) {
    // No input schema defined, use rawInput
    console.log("ℹ️ Using raw input data");
  }

  try {
    // Get validated args if schema exists
    const validArgs = valid("args");
    console.log("✅ Validated args:", validArgs);
  } catch (error) {
    // No args schema defined, use rawArgs
    console.log("ℹ️ Using raw args data");
  }

  return next();
});

Creating Standalone Middleware

You can create reusable middleware functions with createMiddleware:

import { createMiddleware } from "@safekit/safe-fn";

// Reusable middleware functions
const timingMiddleware = createMiddleware(async ({ next }) => {
  return next({ ctx: { requestTime: Date.now() } });
});

const loggingMiddleware = createMiddleware(async ({ rawInput, next }) => {
  console.log("🚀 Request started with:", rawInput);
  const result = await next();
  console.log("✅ Request completed");
  return result;
});

// Use with any client
const clientWithMiddleware = createClient().use(timingMiddleware).use(loggingMiddleware);

Schema Support

safe-fn supports StandardSchema V1 compatible validation libraries with automatic type inference and perfect input/output type distinction. All supported libraries provide both object and tuple schemas with full TypeScript integration.

Supported Validation Libraries

  • Zod v3/v4: Native StandardSchema support
  • Valibot: Native StandardSchema support
  • ArkType: StandardSchema compatible
  • Effect Schema: StandardSchema wrapper (Schema.standardSchemaV1())

Migration from Other Libraries

For libraries that don't natively support StandardSchema V1 (like Yup, Superstruct, Runtypes), you can:

  1. Migrate to a supported library, or
  2. Create StandardSchema V1 wrappers for your existing schemas

For detailed examples of supported validation libraries, see examples/validation-libraries.ts.

What’s Not Included in safe-fn

To keep things simple and flexible, safe-fn leaves out a few features by design:

  • Custom Error Classes: Errors pass through just as your function throws them.
  • Response Serialization: Outputs are returned directly, unmodified.
  • Logging: Integrate your favorite logging solution via middleware if needed.

Philosophy: safe-fn brings type safety and middleware support to your functions, while letting you stay in control of the rest of your stack. It’s here to complement, not replace, your existing setup.

Inspiration

This package was inspired by:

Contributing

See CONTRIBUTING.md for contribution guidelines.

License

MIT © safekit