Skip to content

REST Adapter Configuration

The REST adapter converts procedure definitions into HTTP endpoints. Configure its behavior with adapter options.

import { rest } from '@veloxts/router';
app.register(
rest([userProcedures, postProcedures], {
// Options go here
}),
{ prefix: '/api' } // Fastify prefix option
);

API prefix for all routes. When using server.register(), Fastify’s built-in prefix is recommended:

// RECOMMENDED: Use Fastify's prefix option
app.register(
rest([userProcedures]),
{ prefix: '/api' }
);
// → GET /api/users
// LEGACY: Adapter prefix (for direct invocation)
rest([userProcedures], { prefix: '/v1' })(app);
// → GET /v1/users

Default: /api (legacy mode only)

Generate shortcut routes alongside nested routes for easier access to deeply nested resources.

app.register(
rest([taskProcedures], {
shortcuts: true,
}),
{ prefix: '/api' }
);

Effect:

// With shortcuts: true, generates BOTH:
GET /organizations/:orgId/projects/:projectId/tasks/:id (nested)
GET /tasks/:id (shortcut)
// Collection routes remain nested-only
GET /organizations/:orgId/projects/:projectId/tasks (nested only)

Limitations:

  • Only works for single-resource operations (routes ending with /:id)
  • Collection operations (list*, find*) and creation (create*, add*) require parent context

Default: false

Enable or disable warnings about deep nesting (3+ levels).

app.register(
rest([featureProcedures], {
shortcuts: true,
nestingWarnings: false, // Disable warnings
}),
{ prefix: '/api' }
);

Effect:

// With nestingWarnings: true (default):
// ⚠️ Resource 'features/getFeature' has 4 levels of nesting.
// Consider using shortcuts: true or restructuring your API.
// With nestingWarnings: false:
// (no warnings)

Default: true

Custom error handler for REST endpoints.

app.register(
rest([userProcedures], {
onError: (error, request, reply) => {
console.error('REST error:', error);
// Custom error response
reply.status(500).send({
error: 'Internal Server Error',
message: error instanceof Error ? error.message : 'Unknown error',
});
},
}),
{ prefix: '/api' }
);

Default: Uses Fastify’s default error handling

import { rest, procedures, procedure } from '@veloxts/router';
import { z } from 'zod';
// Organizations at root
const organizationProcedures = procedures('organizations', {
listOrganizations: procedure()
.query(async ({ ctx }) => {
return await ctx.db.organization.findMany();
}),
getOrganization: procedure()
.input(z.object({ id: z.string() }))
.query(async ({ input, ctx }) => {
return await ctx.db.organization.findUniqueOrThrow({
where: { id: input.id },
});
}),
});
// Projects nested under organizations
const projectProcedures = procedures('projects', {
listProjects: procedure()
.parent('organizations')
.input(z.object({ organizationId: z.string() }))
.query(async ({ input, ctx }) => {
return await ctx.db.project.findMany({
where: { organizationId: input.organizationId },
});
}),
});
// Tasks nested under organizations AND projects
const taskProcedures = procedures('tasks', {
listTasks: procedure()
.parents([
{ resource: 'organizations', param: 'orgId' },
{ resource: 'projects', param: 'projectId' },
])
.input(z.object({ orgId: z.string(), projectId: z.string() }))
.query(async ({ input, ctx }) => {
return await ctx.db.task.findMany({
where: { projectId: input.projectId },
});
}),
getTask: procedure()
.parents([
{ resource: 'organizations', param: 'orgId' },
{ resource: 'projects', param: 'projectId' },
])
.input(z.object({
orgId: z.string().optional(),
projectId: z.string().optional(),
id: z.string(),
}))
.query(async ({ input, ctx }) => {
return await ctx.db.task.findUniqueOrThrow({
where: { id: input.id },
});
}),
});
// Register with full configuration
app.register(
rest(
[organizationProcedures, projectProcedures, taskProcedures],
{
shortcuts: true, // Enable shortcut routes for direct resource access
nestingWarnings: true, // Keep warnings enabled (default)
onError: (error, request, reply) => {
// Custom error handling
console.error('REST API error:', {
url: request.url,
method: request.method,
error: error instanceof Error ? error.message : error,
});
reply.status(500).send({
error: 'Internal Server Error',
timestamp: new Date().toISOString(),
});
},
}
),
{ prefix: '/api' }
);

This configuration generates:

GET /api/organizations
GET /api/organizations/:id
GET /api/organizations/:orgId/projects
GET /api/organizations/:orgId/projects/:projectId/tasks
GET /api/organizations/:orgId/projects/:projectId/tasks/:id
GET /api/tasks/:id (shortcut route)

You can register multiple REST adapters with different configurations:

// Public API - standard nesting
app.register(
rest([publicProcedures], {
shortcuts: false,
}),
{ prefix: '/api/v1' }
);
// Admin API - deep nesting with shortcuts
app.register(
rest([adminProcedures], {
shortcuts: true,
nestingWarnings: false, // Suppress warnings (intentional deep nesting)
}),
{ prefix: '/api/admin' }
);
// Internal API - tRPC-only (no REST routes needed)
// Procedures without recognized naming prefixes (get*, list*, create*, etc.)
// simply won't generate REST endpoints.

Each adapter instance is independent and can have its own configuration.

Adjust configuration based on environment:

const isDevelopment = process.env.NODE_ENV === 'development';
const isProduction = process.env.NODE_ENV === 'production';
app.register(
rest([allProcedures], {
shortcuts: true,
nestingWarnings: !isProduction, // Show warnings in dev, suppress in prod
onError: isDevelopment
? (error, request, reply) => {
// Detailed errors in development
reply.status(500).send({
error: error instanceof Error ? error.message : 'Unknown error',
stack: error instanceof Error ? error.stack : undefined,
url: request.url,
method: request.method,
});
}
: undefined, // Use default handler in production
}),
{ prefix: '/api' }
);

Use Fastify’s built-in prefix option instead of adapter prefix:

// GOOD - Fastify prefix
app.register(rest([procs]), { prefix: '/api' });
// AVOID - Adapter prefix (legacy)
rest([procs], { prefix: '/api' })(app);

Fastify prefix integrates better with plugins and middleware.

If your API exceeds 2 levels of nesting, enable shortcuts:

rest([nestedProcedures], { shortcuts: true });

This provides both hierarchical context AND convenient direct access via shortcut routes.

Aim for 2-3 levels maximum. If you need more, consider:

  • Using query parameters for filtering
  • Flattening your resource hierarchy
  • Breaking into separate API endpoints

Only set nestingWarnings: false when:

  • Deep nesting is intentional and cannot be avoided
  • You’ve enabled shortcuts to mitigate complexity
  • Your API design has been thoroughly reviewed

Don’t disable warnings just to silence them during development.

Prefer Fastify’s global error handler over per-adapter handlers:

// GOOD - Global handler
app.setErrorHandler((error, request, reply) => {
// Handle all errors consistently
});
// AVOID - Per-adapter handler (unless truly needed)
rest([procs], {
onError: (error, request, reply) => { /* ... */ },
});

Cause: Procedures don’t match naming conventions and lack .rest() overrides

Fix: Check console for warnings, verify naming patterns, or add .rest() configuration

Cause: Shortcut route path collides with root resource

Example:

// Conflict: /tasks/:id exists as both root and shortcut route
procedures('tasks', {
getTask: ..., // → GET /tasks/:id (root)
});
procedures('nestedTasks', {
getNestedTask: procedure()
.parent('projects')
.query(...), // → GET /tasks/:id (shortcut - CONFLICT!)
});

Fix: Rename resources to avoid collision or disable shortcuts

Cause: Multiple adapter registrations, only some have nestingWarnings: false

Fix: Ensure all REST adapter registrations use nestingWarnings: false