Procedures
Procedures are the core building block of Velox TS APIs. They define type-safe endpoints with input validation, output schemas, and handlers.
Basic Syntax
Section titled “Basic Syntax”import { procedures, procedure, resourceSchema, resource } from '@veloxts/velox';import { z } from '@veloxts/velox';
// Define resource schema with field visibilityconst UserSchema = resourceSchema() .public('id', z.string().uuid()) .public('name', z.string()) .authenticated('email', z.string().email()) .admin('internalNotes', z.string().nullable()) .build();
export const userProcedures = procedures('users', { getUser: procedure() .input(z.object({ id: z.string() })) .query(async ({ input, ctx }) => { const user = await ctx.db.user.findUniqueOrThrow({ where: { id: input.id } }); return resource(user, UserSchema.public); // returns { id, name } }),});Builder Methods
Section titled “Builder Methods”.input(schema)
Section titled “.input(schema)”Define the input validation schema:
.input(z.object({ id: z.string().uuid(), includeDeleted: z.boolean().optional(),})).output(schema) with Resource Schemas
Section titled “.output(schema) with Resource Schemas”The .output() method accepts both plain Zod schemas and tagged resource views for context-dependent output projection. The Resource API lets you define field visibility levels (public, authenticated, admin) once and project data based on access level.
const UserSchema = resourceSchema() .public('id', z.string()) .public('name', z.string()) .authenticated('email', z.string()) .admin('internalNotes', z.string().nullable()) .build();
// Tagged view — one-liner projectiongetPublicProfile: procedure() .output(UserSchema.public) // projects { id, name } .query(async ({ input, ctx }) => { return ctx.db.user.findUniqueOrThrow({ where: { id: input.id } }); }),
// Automatic projection with guardsgetProfile: procedure() .guard(authenticated) .output(UserSchema.authenticated) // projects { id, name, email } .query(async ({ input, ctx }) => { return ctx.db.user.findUniqueOrThrow({ where: { id: input.id } }); }),See Resource API for tagged views, automatic projection, manual projection, collections, and type inference.
For domain-specific roles beyond the default three levels, defineAccessLevels() lets you define arbitrary level names and named groups. The same tagged view pattern applies — ArticleSchema.reviewer, ArticleSchema.moderator, and so on. See Custom Access Levels for details.
.query(handler) / .mutation(handler)
Section titled “.query(handler) / .mutation(handler)”Define the handler function:
.query()- For read operations (GET).mutation()- For write operations (POST, PUT, DELETE)
.query(({ input, ctx }) => ctx.db.user.findUniqueOrThrow({ where: { id: input.id }})).guard(guardFn)
Section titled “.guard(guardFn)”Add authorization guards:
.guard(authenticated).guard(hasRole('admin')).use(middleware)
Section titled “.use(middleware)”Add middleware:
.use(rateLimitMiddleware).use(loggingMiddleware).policy(action)
Section titled “.policy(action)”Attach a policy action for resource-level authorization:
import { PostPolicy } from '../policies/post.js';
.guard(authenticated).policy(PostPolicy.update).mutation(async ({ input, ctx }) => { ... })See Policies for defining policies and actions.
.throws(...errors)
Section titled “.throws(...errors)”Declare which domain errors a procedure can throw. This enables type-safe error narrowing on the client:
import { InsufficientBalance, AccountLocked } from '../errors.js';
.throws(InsufficientBalance, AccountLocked).mutation(async ({ input, ctx }) => { ... })See Business Logic — Domain Errors for details.
.transactional(options?)
Section titled “.transactional(options?)”Wrap the handler in a database transaction. Rolls back automatically on error:
.transactional().mutation(async ({ input, ctx }) => { // ctx.db is scoped to the transaction})See Business Logic — Transactions for options.
.through(...steps)
Section titled “.through(...steps)”Run a pre-handler pipeline of steps, with optional revert-on-failure:
.through(reserveInventory, chargePayment, sendConfirmation).mutation(async ({ input, ctx }) => { ... })Steps execute in order. If one fails, earlier steps revert. See Business Logic — Pipelines for defineStep and .onRevert().
.emits(Event, mapper?)
Section titled “.emits(Event, mapper?)”Emit a domain event after the handler succeeds:
.emits(OrderPlaced, (output) => ({ orderId: output.id })).mutation(async ({ input, ctx }) => { ... })See Business Logic — Domain Events for event definitions.
.useAfter(hook)
Section titled “.useAfter(hook)”Add a post-handler hook that runs after the response is sent:
.useAfter(async ({ input, output, ctx }) => { await ctx.analytics.track('order.placed', { orderId: output.id });})See Middleware — Post-Handler Hooks for details.
.rest(options)
Section titled “.rest(options)”Override REST endpoint generation:
.rest({ method: 'POST', path: '/auth/login' })See REST Overrides for full options.
Chain Overview
Section titled “Chain Overview”Every builder method in declaration order:
| Method | Purpose | Guide |
|---|---|---|
.input(schema) | Input validation | above |
.output(schema) | Output schema or resource view | above, Resource API |
.guard(guard) | Request-level authorization | Guards |
.policy(action) | Resource-level authorization | Policies |
.use(middleware) | Pre-handler middleware | Middleware |
.throws(...errors) | Declare domain errors | Business Logic |
.transactional(opts?) | Wrap handler in DB transaction | Business Logic |
.through(...steps) | Pre-handler pipeline | Business Logic |
.emits(Event, mapper?) | Emit domain event on success | Business Logic |
.rest(config) | Override REST route | REST Overrides |
.query(handler) / .mutation(handler) | Terminal handler | above |
.useAfter(hook) | Post-handler hook | Middleware |
Handler Context
Section titled “Handler Context”The handler receives { input, ctx }:
.query(({ input, ctx }) => { // input: Validated input data // ctx.db: Prisma client // ctx.request: Fastify request // ctx.reply: Fastify reply // ctx.user: Authenticated user (if using auth)})Procedure Collections
Section titled “Procedure Collections”Group related procedures with procedures():
export const postProcedures = procedures('posts', { listPosts: procedure()..., getPost: procedure()..., createPost: procedure()..., updatePost: procedure()..., deletePost: procedure()...,});The first argument ('posts') becomes:
- The REST resource name:
/api/posts - The tRPC namespace:
trpc.posts.listPosts
Type Inference
Section titled “Type Inference”Types flow automatically through the Resource API:
// Backend - Define resource schemaconst UserSchema = resourceSchema() .public('id', z.string()) .public('name', z.string()) .authenticated('email', z.string()) .build();
const createUser = procedure() .input(z.object({ name: z.string(), email: z.string().email() })) .mutation(async ({ input, ctx }) => { const user = await ctx.db.user.create({ data: input }); return resource(user, UserSchema.authenticated); });
// Frontend (via tRPC or client)// Input is typed: { name: string; email: string }// Output is typed: { id: string; name: string; email: string }Discovery
Section titled “Discovery”Procedures are automatically discovered from src/procedures/:
import { veloxApp, rest, discoverProcedures } from '@veloxts/velox';
const app = await veloxApp({ port: 3030 });const collections = await discoverProcedures('./src/procedures');app.routes(rest([...collections], { prefix: '/api' }));Related Content
Section titled “Related Content”- Business Logic - Transactions, domain errors, events, pipelines
- Resource API - Field-level visibility and projection
- REST Conventions - How names map to HTTP
- Guards - Authorization
- Middleware - Request processing
- Architectural Patterns - Scale from prototype to enterprise