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

@archtx/procedures

v6.2.0

Published

Procedure generator for @archtx

Readme

@archtx/procedures

A TypeScript library for creating type-safe, schema-validated procedure calls (RPCs, API controllers, etc.) with a single function definition.

Quick Start

npm install @archtx/procedures
import { Procedures } from '@archtx/procedures'
import { v } from 'suretype' // or use TypeBox

const { Create } = Procedures()

// Define a procedure with schema validation
const { GetUser, procedure, info } = Create(
  'GetUser',
  {
    description: 'Fetch a user by ID',
    schema: {
      args: v.object({ id: v.number().required() }),
      data: v.object({ name: v.string(), email: v.string() }),
    },
  },
  async (ctx, args) => {
    // args is typed as { id: number }
    return { name: 'John', email: '[email protected]' }
  },
)

// Call the procedure directly
const user = await GetUser({}, { id: 1 })

Features

  • Single-function procedure definitions with Create()
  • Type-safe context, arguments, and return values
  • Built-in schema validation (Suretype or TypeBox)
  • Automatic JSON Schema generation for documentation
  • Pre-handler hooks for authentication/authorization
  • Typed error handling with HTTP-style status codes
  • Framework-agnostic registration callbacks

Core Concepts

The Procedures Factory

Procedures() creates a scoped procedure factory with shared configuration:

const { Create, getProcedures } = Procedures({
  onCreate: (registration) => {
    // Called when each procedure is created
    // Use this to register with your router/framework
  },
})

Create Function Signature

Create(
  name: string,           // Unique procedure name
  config: ProcedureConfig, // Schema, hooks, description, extended config
  handler: HandlerFunction // Async function that executes the procedure
)

Returns an object with:

  • [name]: Handler function (dynamic key matching the procedure name)
  • procedure: Same handler function (generic reference)
  • info: Metadata object with schema, description, and config

Configuration Options

Custom Context Type

Define a context type that will be passed to all handlers:

interface AppContext {
  authToken: string
  requestId: string
}

const { Create } = Procedures<AppContext>()

Create('MyProcedure', {}, async (ctx, args) => {
  // ctx.authToken and ctx.requestId are typed
  return ctx.authToken
})

Extended Config Type

Add custom configuration properties to all procedures:

interface ApiConfig {
  route: string
  method: 'GET' | 'POST' | 'PUT' | 'DELETE'
  rateLimit?: number
}

const { Create } = Procedures<unknown, ApiConfig>({
  onCreate: ({ name, config }) => {
    // config.route and config.method are available
    router[config.method.toLowerCase()](config.route, config.handler)
  },
})

const { info } = Create(
  'ListUsers',
  {
    route: '/api/users', // type-checked & required by ApiConfig
    method: 'GET', // type-checked & required by ApiConfig
    rateLimit: 100, // type-checked & optional by ApiConfig
  },
  async () => []
)

console.log(info.route) // '/api/users'

Registration Callback (onCreate)

Use onCreate to integrate with your framework:

const { Create } = Procedures({
  onCreate: ({ handler, config, name }) => {
    // Register with Express
    app.post(`/rpc/${name}`, async (req, res) => {
      try {
        const result = await handler(req.context, req.body)
        res.json(result)
      } catch (error) {
        res.status(error.code).json({ error: error.message })
      }
    })
  },
})

Schema Validation

Schemas provide type inference and runtime validation. Supports both Suretype and TypeBox.

Using Suretype

import { v } from 'suretype'

Create('CreateUser', {
  schema: {
    args: v.object({
      name: v.string().required(),
      email: v.string().required(),
      age: v.number(),
    }),
    data: v.object({
      id: v.string(),
      name: v.string(),
    }),
  },
}, async (ctx, args) => {
  // args: { name: string, email: string, age?: number }
  return { id: 'user-123', name: args.name }
})

Using TypeBox

import { Type } from 'typebox'

Create('CreateUser', {
  schema: {
    args: Type.Object({
      name: Type.String(),
      email: Type.String(),
      age: Type.Optional(Type.Number()),
    }),
    data: Type.Object({
      id: Type.String(),
      name: Type.String(),
    }),
  },
}, async (ctx, args) => {
  return { id: 'user-123', name: args.name }
})

Validation Behavior

When arguments fail validation, a ProcedureValidationError is thrown automatically:

try {
  await CreateUser({}, { name: 'John' }) // missing required 'email'
} catch (error) {
  // error instanceof ProcedureValidationError
  // error.code === 422 (VALIDATION_ERROR)
  // error.errors contains detailed validation errors
}

Accessing Generated JSON Schema

The info object contains the generated JSON Schema:

const { info } = Create('GetUser', {
  schema: {
    args: v.object({ id: v.number().required() }),
  },
}, handler)

console.log(info.schema)
// {
//   args: { type: 'object', properties: { id: { type: 'number' } }, required: ['id'] },
//   data: undefined
// }

Hooks

Hooks run before the handler and can:

  • Perform authentication/authorization
  • Inject additional context
  • Throw errors to prevent handler execution

Basic Hook

Create('ProtectedResource', {
  hook: async (ctx) => {
    // Return additional context that merges into handler ctx
    return { timestamp: Date.now() }
  },
}, async (ctx, args) => {
  // ctx.timestamp is available and typed
  return ctx.timestamp
})

Authentication Hook

interface AuthContext {
  authToken: string
}

const { Create } = Procedures<AuthContext>()

Create('GetProfile', {
  hook: async (ctx) => {
    if (!isValidToken(ctx.authToken)) {
      throw ctx.error(ProcedureCodes.UNAUTHORIZED, 'Invalid token')
    }

    const user = await getUserFromToken(ctx.authToken)
    return { user } // Adds 'user' to handler context
  },
}, async (ctx) => {
  // ctx.user is typed from hook return
  return { profile: ctx.user.profile }
})

Hook Error Handling

  • Throwing ctx.error() throws a ProcedureError with your code/message
  • Throwing any other error wraps it in ProcedureHookError (code: 412)
hook: async (ctx) => {
  // This throws ProcedureError with code 401
  throw ctx.error(401, 'Unauthorized')

  // This would throw ProcedureHookError with code 412
  throw new Error('Something went wrong')
}

Error Handling

Error Types

| Error Class | Code | When Thrown | |-------------|------|-------------| | ProcedureError | Custom | Via ctx.error() in hooks/handlers | | ProcedureHookError | 412 | Unhandled errors in hooks | | ProcedureValidationError | 422 | Schema validation failures | | ProcedureRegistrationError | N/A | Invalid schema during registration |

Using ctx.error()

Create typed errors with HTTP-style codes:

async (ctx, args) => {
  const user = await findUser(args.id)

  if (!user) {
    throw ctx.error(ProcedureCodes.NOT_FOUND, 'User not found', { id: args.id })
  }

  return user
}

ProcedureCodes

HTTP-inspired status codes:

import { ProcedureCodes } from '@archtx/procedures'

// Success codes
ProcedureCodes.OK                // 200
ProcedureCodes.CREATED           // 201
ProcedureCodes.NO_CONTENT        // 204

// Client error codes
ProcedureCodes.BAD_REQUEST       // 400
ProcedureCodes.UNAUTHORIZED      // 401
ProcedureCodes.FORBIDDEN         // 403
ProcedureCodes.NOT_FOUND         // 404
ProcedureCodes.CONFLICT          // 409
ProcedureCodes.VALIDATION_ERROR  // 422
ProcedureCodes.TOO_MANY_REQUESTS // 429

// Server error codes
ProcedureCodes.INTERNAL_ERROR    // 500
ProcedureCodes.HANDLER_ERROR     // 500
ProcedureCodes.NOT_IMPLEMENTED   // 501
ProcedureCodes.SERVICE_UNAVAILABLE // 503

Catching Errors

import { ProcedureError, ProcedureValidationError } from '@archtx/procedures'

try {
  await MyProcedure(ctx, args)
} catch (error) {
  if (error instanceof ProcedureValidationError) {
    // Handle validation errors
    console.log(error.errors) // Array of validation errors
  } else if (error instanceof ProcedureError) {
    // Handle procedure errors
    console.log(error.code)          // HTTP-style code
    console.log(error.message)       // Error message
    console.log(error.procedureName) // 'MyProcedure'
    console.log(error.meta)          // Optional metadata
  }
}

Advanced Usage

Calling Procedures from Other Procedures

Procedures can call each other while maintaining context isolation:

const { GetUser } = Create('GetUser', {
  hook: async () => ({ source: 'GetUser' }),
}, async (ctx, args) => {
  return { id: args.id, source: ctx.source }
})

const { GetUserWithPosts } = Create('GetUserWithPosts', {
  hook: async () => ({ source: 'GetUserWithPosts' }),
}, async (ctx, args) => {
  // Call another procedure - it runs with its own hook context
  const user = await GetUser(ctx, { id: args.userId })
  // user.source === 'GetUser' (not 'GetUserWithPosts')
  return { user, posts: [] }
})

Retrieving Registered Procedures

Use getProcedures() for introspection, documentation generation, or testing:

const { Create, getProcedures } = Procedures()

Create('UserCreate', { schema: { args: v.object({ name: v.string() }) } }, handler)
Create('UserDelete', { schema: { args: v.object({ id: v.number() }) } }, handler)

const procedures = getProcedures()
// Map<string, { name, config, handler }>

procedures.forEach((proc, name) => {
  console.log(`${name}: ${JSON.stringify(proc.config.schema)}`)
})

Framework Integration Example

// procedures.ts
import { Procedures } from '@archtx/procedures'
import { v } from 'suretype'

interface RequestContext {
  userId: string
  requestId: string
}

interface RouteConfig {
  path: string
  method: 'GET' | 'POST' | 'PUT' | 'DELETE'
}

export const { Create, getProcedures } = Procedures<RequestContext, RouteConfig>({
  onCreate: ({ handler, config, name }) => {
    // Register with your router here
    console.log(`Registered: ${config.method} ${config.path} -> ${name}`)
  },
})

// user-procedures.ts
import { Create } from './procedures'

export const { GetUser } = Create(
  'GetUser',
  {
    path: '/api/users/:id',
    method: 'GET',
    description: 'Fetch user by ID',
    schema: {
      args: v.object({ id: v.string().required() }),
      data: v.object({ id: v.string(), name: v.string() }),
    },
    hook: async (ctx) => {
      // Verify user has permission
      if (!hasPermission(ctx.userId, 'read:users')) {
        throw ctx.error(403, 'Forbidden')
      }
      return {}
    },
  },
  async (ctx, args) => {
    return await db.users.findById(args.id)
  },
)

Validation Access via Config

The onCreate callback receives validation functions for external use:

const { Create } = Procedures({
  onCreate: ({ config }) => {
    if (config.validation?.args) {
      // Pre-validate before calling handler
      const { errors } = config.validation.args(requestBody)
      if (errors) {
        return res.status(422).json({ errors })
      }
    }
  },
})

API Reference

Procedures(builder?)

Creates a procedure factory.

Parameters:

  • builder.onCreate?: (registration) => void - Called when each procedure is created

Returns:

  • Create - Function to create procedures
  • getProcedures - Returns Map of all registered procedures

Create(name, config, handler)

Creates a procedure.

Parameters:

  • name: string - Unique procedure identifier
  • config.description?: string - Human-readable description
  • config.schema?.args - Suretype/TypeBox schema for arguments
  • config.schema?.data - Suretype/TypeBox schema for return value
  • config.hook?: (ctx, args) => Promise<LocalContext> - Pre-handler hook
  • handler: (ctx, args) => Promise<Data> - Procedure implementation

Returns:

  • [name]: handler - Named handler export
  • procedure: handler - Generic handler reference
  • info - Procedure metadata with compiled schema

License

MIT