ts-procedures
v2.1.1
Published
A TypeScript RPC framework that creates type-safe, schema-validated procedure calls with a single function definition. Define your procedures once and get full type inference, runtime validation, and framework integration hooks.
Downloads
621
Maintainers
Readme
ts-procedures
A TypeScript RPC framework that creates type-safe, schema-validated procedure calls with a single function definition. Define your procedures once and get full type inference, runtime validation and procedure documentation/configuration.
Installation
npm install ts-proceduresQuick Start
import { Procedures } from 'ts-procedures'
import { Type } from 'typebox'
// Create a procedures factory
const { Create } = Procedures()
// Define a procedure with schema validation
const { GetUser, procedure, info } = Create(
'GetUser',
{
description: 'Fetches a user by ID',
schema: {
params: Type.Object({ userId: Type.String() }),
returnType: Type.Object({ id: Type.String(), name: Type.String() }),
},
},
async (ctx, params /* typed as { userId: string } */) => {
// returnType is inferred as { id: string; name: string }
return { id: params.userId, name: 'John Doe' }
},
)
// Call the procedure directly
const user = await GetUser({}, { userId: '123' })
// Or use the generic reference
const user2 = await procedure({}, { userId: '456' })Core Concepts
Procedures Factory
The Procedures() function creates a factory for defining procedures. It accepts two generic type parameters:
Procedures<TContext, TExtendedConfig>(builder?: {
onCreate?: (procedure: TProcedureRegistration<TContext, TExtendedConfig>) => void
})| Parameter | Description |
|-----------|----------------------------------------------------------------------------|
| TContext | The base context type passed to all handlers as the first parameter |
| TExtendedConfig | Additional configuration properties for all procedures config properties |
| builder.onCreate | Optional callback invoked when each procedure is registered (runtime) |
Create Function
The Create function defines individual procedures:
Create(name, config, handler)Returns:
{ [name]: handler }- Named export for the handlerprocedure- Generic reference to the handlerinfo- Procedure meta (name, description, schema,TExtendedConfigproperties, etc.)
Using Generics
Base Context
Define a shared context type for all procedures in your application:
interface AppContext {
authToken: string
requestId: string
logger: Logger
}
const { Create } = Procedures<AppContext>()
const { SecureEndpoint } = Create(
'SecureEndpoint',
{},
async (ctx, params) => {
// ctx.authToken is typed as string
// ctx.requestId is typed as string
// ctx.logger is typed as Logger
return { token: ctx.authToken }
},
)
// When calling, you must provide the context
await SecureEndpoint({ authToken: 'abc', requestId: '123', logger: myLogger }, {})Extended Configuration
Add custom properties to all procedure configs:
interface ExtendedConfig {
permissions: string[]
rateLimit?: number
cacheTTL?: number
}
const { Create } = Procedures<AppContext, ExtendedConfig>()
const { AdminOnly } = Create(
'AdminOnly',
{
permissions: ['admin'], // Required by ExtendedConfig
rateLimit: 100, // Optional
description: 'Admin-only endpoint',
},
async (ctx, params) => {
return { admin: true }
},
)
// Access extended config via info
console.log(AdminOnly.info.permissions) // ['admin']Combined Example
interface CustomContext {
authToken: string
tenantId: string
}
interface ExtendedConfig {
requiresAuth: boolean
auditLog?: boolean
}
const { Create, getProcedures } = Procedures<CustomContext, ExtendedConfig>({
onCreate: (procedure) => {
// Register with your framework
console.log(`Registered: ${procedure.name}`)
console.log(`Requires Auth: ${procedure.config.requiresAuth}`)
},
})
const { CreateUser } = Create(
'CreateUser',
{
requiresAuth: true,
auditLog: true,
description: 'Creates a new user',
schema: {
params: Type.Object({
email: Type.String(),
name: Type.String(),
}),
returnType: Type.Object({ id: Type.String() }),
},
},
async (ctx, params) => {
// Both context and params are fully typed
return { id: 'user-123' }
},
)Schema Validation
Suretype
import { v } from 'suretype'
Create(
'CreatePost',
{
schema: {
params: Type.Object({
title: Type.String(),
content: Type.String(),
tags: Type.array(Type.String()),
}),
returnType: Type.Object({
id: Type.String(),
createdAt: Type.String(),
}),
},
},
async (ctx, params) => {
// params typed as { title: string, content: string, tags?: string[] }
return { id: '1', createdAt: new Date().toISOString() }
},
)TypeBox
import { Type } from 'typebox'
Create(
'CreatePost',
{
schema: {
params: Type.Object({
title: Type.String(),
content: Type.String(),
tags: Type.Optional(Type.Array(Type.String())),
}),
returnType: Type.Object({
id: Type.String(),
createdAt: Type.String(),
}),
},
},
async (ctx, params) => {
// params typed as { title: string, content: string, tags?: string[] }
return { id: '1', createdAt: new Date().toISOString() }
},
)Validation Behavior
AJV is configured with:
allErrors: true- Report all validation errorscoerceTypes: true- Automatically coerce types when possibleremoveAdditional: true- Strip properties not in schema
Note: schema.params is validated at runtime. schema.returnType is for documentation/introspection only.
Error Handling
Using ctx.error()
The error() function is injected into both hooks and handlers:
Create(
'GetResource',
{},
async (ctx, params) => {
const resource = await db.find(params.id)
if (!resource) {
throw ctx.error(404, 'Resource not found', { id: params.id })
}
return resource
},
)Error Handling
| Error Class | Trigger |
|-------------|---------|
| ProcedureError | ctx.error() in handlers |
| ProcedureValidationError | Schema validation failure |
| ProcedureRegistrationError | Invalid schema at registration |
Error Properties
try {
await MyProcedure(ctx, params)
} catch (e) {
if (e instanceof ProcedureError) {
console.log(e.procedureName) // 'MyProcedure'
console.log(e.message) // 'Resource not found'
console.log(e.meta) // { id: '123' }
}
}Framework Integration
onCreate Callback
Register procedures with your framework (Express, Fastify, etc.):
import express from 'express'
const app = express()
const routes: Map<string, Function> = new Map()
const { Create } = Procedures<{ req: Request; res: Response }>({
onCreate: ({ name, handler, config }) => {
// Register as Express route
app.post(`/rpc/${name}`, async (req, res) => {
try {
const result = await handler({ req, res }, req.body)
res.json(result)
} catch (e) {
if (e instanceof ProcedureError) {
res.status(500).json({ error: e.message })
} else {
res.status(500).json({ error: 'Internal error' })
}
}
})
},
})
// Procedures are automatically registered as /rpc/GetUser, /rpc/CreateUser, etc.Express RPC Integration
ts-procedures includes an RPC-style HTTP integration for Express that creates POST routes at /rpc/{name}/{version} paths with automatic JSON schema documentation.
import { ExpressRPCAppBuilder, RPCConfig } from 'ts-procedures/express-rpc'
// Create procedure factory with RPC config
const RPC = Procedures<AppContext, RPCConfig>()
// Define procedures with name and version
RPC.Create(
'GetUser',
{
name: ['users', 'get'],
version: 1,
schema: {
params: Type.Object({ id: Type.String() }),
returnType: Type.Object({ id: Type.String(), name: Type.String() }),
},
},
async (ctx, params) => {
return { id: params.id, name: 'John Doe' }
}
)
// Build Express app with registered procedures
const app = new ExpressRPCAppBuilder()
.register(RPC, (req) => ({ userId: req.headers['x-user-id'] as string }))
.build()
app.listen(3000)
// Route created: POST /rpc/users/get/1See Express RPC Integration Guide for complete setup instructions including lifecycle hooks, error handling, and route documentation.
Introspection with getProcedures()
Access all registered procedures for documentation or routing:
const { Create, getProcedures } = Procedures()
Create('GetUser', { schema: { params: Type.Object({ id: Type.String() }) } }, async () => {})
Create('ListUsers', { schema: { params: Type.Object({}) } }, async () => {})
// Get all registered procedures
const procedures = getProcedures()
// Generate OpenAPI spec
for (const config of procedures) {
console.log(`${config.name}:`, config.schema)
}Testing
Procedures return handlers that can be called directly in tests:
import { describe, test, expect } from 'vitest'
import { Procedures } from 'ts-procedures'
import { Type } from 'typebox'
interface MyCustomContext {
userId?: string
userName?: string
}
const { Create } = Procedures<MyCustomContext>()
const { GetUser, info } = Create(
'GetUser',
{
schema: {
params: Type.Object({ hideName: Type.Optional(Type.Boolean()) }),
returnType: Type.Object({ id: Type.String(), name: Type.String() }),
},
},
async (ctx, params) => {
if (!params.userName || !ctx.userId) {
throw ctx.error('User is not authenticated')
}
return {
id: params.userId,
name: params?.hideName ? '*******' : params.userName
}
},
)
describe('GetUser', () => {
test('returns user', async () => {
const result = await GetUser({userId:'123',userName:'Ray'}, { hideName: false })
expect(result).toEqual({ id: '123', name: 'Ray' })
})
test('hides user name', async () => {
const result = await GetUser({userId:'123',userName:'Ray'}, { hideName: true })
expect(result).toEqual({ id: '123', name: '*******' })
})
test('validates params', async () => {
await expect(GetUser({}, {})).rejects.toThrow(ProcedureValidationError)
})
test('has correct schema', () => {
expect(info.schema.params).toEqual({
type: 'object',
properties: { id: { type: 'string' } },
required: ['id'],
})
})
})API Reference
Procedures(builder?)
Creates a procedure factory.
Parameters:
builder.onCreate- Callback invoked when each procedure is registered
Returns:
Create- Function to define proceduresgetProcedures()- ReturnsArrayof all registered procedures
Create(name, config, handler)
Defines a procedure.
Parameters:
name- Unique procedure name (becomes named export)config.description- Optional descriptionconfig.schema.params- Suretype or TypeBox schema for params (validated at runtime)config.schema.returnType- Suretype or TypeBox schema for return returnType (documentation only)- Additional properties from
TExtendedConfig handler- Async function(ctx, params) => Promise<returnType>
Returns:
{ [name]: handler }- Named handler exportprocedure- Generic handler referenceinfo- Procedure metareturnType
Type Exports
import {
// Core
Procedures,
// Errors
ProcedureError,
ProcedureValidationError,
ProcedureRegistrationError,
// Types
TLocalContext,
TProcedureRegistration,
TNoContextProvided,
// Schema utilities
extractJsonSchema,
schemaParser,
isTypeboxSchema,
isSuretypeSchema,
// Schema types
TJSONSchema,
TSchemaLib,
TSchemaParsed,
TSchemaValidationError,
Prettify,
} from 'ts-procedures'License
MIT
