Skip to content

Hybrid Architecture

The hybrid architecture generates both REST and tRPC endpoints from the same procedure definitions. Your mobile team gets a standard REST API with OpenAPI docs, while your web dashboard talks directly over tRPC with full type safety. No code duplication, no separate API layers — one procedure serves both transports.

  • Mobile apps (REST) + web dashboard (tRPC)
  • Public API (REST) + internal tools (tRPC)
  • Gradual migration from tRPC to REST
  • Different teams with different needs

Velox TS generates both REST and tRPC endpoints from the same procedures:

import { resourceSchema, resourceCollection } from '@veloxts/router';
const OrderSchema = resourceSchema()
.public('id', z.string())
.public('total', z.number())
.authenticated('userId', z.string())
.admin('paymentDetails', z.object({ /* ... */ }))
.build();
export const orderProcedures = procedures('orders', {
// Available as:
// - REST: GET /api/orders
// - tRPC: trpc.orders.listOrders.query()
listOrders: procedure()
.query(async ({ ctx }) => {
const orders = await ctx.db.order.findMany();
return resourceCollection(orders, OrderSchema.authenticated);
}),
});
// iOS Swift
let url = URL(string: "http://api.example.com/api/orders")!
let (data, _) = try await URLSession.shared.data(from: url)
let orders = try JSONDecoder().decode([Order].self, from: data)
// React with tRPC
const { data: orders } = trpc.orders.listOrders.useQuery();
Terminal window
curl https://api.example.com/api/orders \
-H "Authorization: Bearer ${API_KEY}"

Procedures with recognized naming convention prefixes (e.g., get*, list*, create*) generate both tRPC and REST endpoints. Procedures without a recognized prefix are tRPC-only:

// tRPC + REST (naming convention matches)
export const publicProcedures = procedures('status', {
getStatus: procedure()
.query(...),
// → tRPC: trpc.status.getStatus
// → REST: GET /api/status/:id
});
// tRPC-only (no naming convention match)
export const internalProcedures = procedures('internal', {
syncMetrics: procedure()
.query(...),
// → tRPC: trpc.internal.syncMetrics
// → No REST endpoint generated
});

The same guards work for both:

const getOrder = procedure()
.guard(authenticated)
.input(z.object({ id: z.string() }))
.query(...)
// REST: Authorization header
// tRPC: Same header or cookie

For REST clients that need versioning:

export const v1Procedures = procedures('v1/orders', {
listOrders: procedure()...
});
export const v2Procedures = procedures('v2/orders', {
listOrders: procedure()... // New shape
});