@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/proceduresimport { 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 aProcedureErrorwith 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 // 503Catching 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 proceduresgetProcedures- Returns Map of all registered procedures
Create(name, config, handler)
Creates a procedure.
Parameters:
name: string- Unique procedure identifierconfig.description?: string- Human-readable descriptionconfig.schema?.args- Suretype/TypeBox schema for argumentsconfig.schema?.data- Suretype/TypeBox schema for return valueconfig.hook?: (ctx, args) => Promise<LocalContext>- Pre-handler hookhandler: (ctx, args) => Promise<Data>- Procedure implementation
Returns:
[name]: handler- Named handler exportprocedure: handler- Generic handler referenceinfo- Procedure metadata with compiled schema
License
MIT
