Procedure generator for @archtx
npm install @archtx/proceduresA TypeScript library for creating type-safe, schema-validated procedure calls (RPCs, API controllers, etc.) with a single function definition.
``bash`
npm install @archtx/procedures
`ts
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: 'john@example.com' }
},
)
// Call the procedure directly
const user = await GetUser({}, { id: 1 })
`
- 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
---
Procedures() creates a scoped procedure factory with shared configuration:
`ts`
const { Create, getProcedures } = Procedures({
onCreate: (registration) => {
// Called when each procedure is created
// Use this to register with your router/framework
},
})
`ts`
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
---
Define a context type that will be passed to all handlers:
`ts
interface AppContext {
authToken: string
requestId: string
}
const { Create } = Procedures
Create('MyProcedure', {}, async (ctx, args) => {
// ctx.authToken and ctx.requestId are typed
return ctx.authToken
})
`
Add custom configuration properties to all procedures:
`ts
interface ApiConfig {
route: string
method: 'GET' | 'POST' | 'PUT' | 'DELETE'
rateLimit?: number
}
const { Create } = Procedures
onCreate: ({ name, config }) => {
// config.route and config.method are available
routerconfig.method.toLowerCase()
},
})
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'
`
Use onCreate to integrate with your framework:
`ts/rpc/${name}
const { Create } = Procedures({
onCreate: ({ handler, config, name }) => {
// Register with Express
app.post(, 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 })
}
})
},
})
---
Schemas provide type inference and runtime validation. Supports both Suretype and TypeBox.
`ts
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 }
})
`
`ts
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 }
})
`
When arguments fail validation, a ProcedureValidationError is thrown automatically:
`ts`
try {
await CreateUser({}, { name: 'John' }) // missing required 'email'
} catch (error) {
// error instanceof ProcedureValidationError
// error.code === 422 (VALIDATION_ERROR)
// error.errors contains detailed validation errors
}
The info object contains the generated JSON Schema:
`ts
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 run before the handler and can:
- Perform authentication/authorization
- Inject additional context
- Throw errors to prevent handler execution
`ts`
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
})
`ts
interface AuthContext {
authToken: string
}
const { Create } = Procedures
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 }
})
`
- Throwing ctx.error() throws a ProcedureError with your code/messageProcedureHookError
- Throwing any other error wraps it in (code: 412)
`ts
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 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 |
Create typed errors with HTTP-style codes:
`ts
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
}
`
HTTP-inspired status codes:
`ts
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
`
`ts
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
}
}
`
---
Procedures can call each other while maintaining context isolation:
`ts
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: [] }
})
`
Use getProcedures() for introspection, documentation generation, or testing:
`ts
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
procedures.forEach((proc, name) => {
console.log(${name}: ${JSON.stringify(proc.config.schema)})`
})
`ts
// 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
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)
},
)
`
The onCreate callback receives validation functions for external use:
`ts`
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 })
}
}
},
})
---
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
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
- - Pre-handler hookhandler: (ctx, args) => Promise
- - Procedure implementation
Returns:
- [name]: handler - Named handler exportprocedure: handler
- - Generic handler referenceinfo` - Procedure metadata with compiled schema
-
---
MIT