Skip to content

tRPC API Architecture

Use this architecture when all your clients are TypeScript and you want maximum type safety.

  • All clients are TypeScript
  • Maximum type safety is priority
  • Internal APIs between services
  • No need for REST/OpenAPI
Terminal window
npx create-velox-app my-api --trpc

With --trpc, VeloxTS skips REST generation. You get pure tRPC:

src/procedures/users.ts
import { procedures, procedure } from '@veloxts/velox';
import { z } from '@veloxts/velox';
export const userProcedures = procedures('users', {
list: procedure()
.output(z.array(UserSchema))
.query(({ ctx }) => ctx.db.user.findMany()),
get: procedure()
.input(z.object({ id: z.string() }))
.query(({ input, ctx }) => ctx.db.user.findUniqueOrThrow({ where: { id: input.id } })),
create: procedure()
.input(CreateUserSchema)
.mutation(({ input, ctx }) => ctx.db.user.create({ data: input })),
});

The frontend imports types directly from the backend:

frontend/src/api.ts
import { createTRPCClient } from '@trpc/client';
import type { AppRouter } from '../../api/src/router';
export const trpc = createTRPCClient<AppRouter>({
links: [
httpBatchLink({
url: 'http://localhost:3030/trpc',
}),
],
});
// Fully typed!
const users = await trpc.users.list.query();
const user = await trpc.users.get.query({ id: '123' });

Types flow automatically:

// Backend defines the shape
const createUser = procedure()
.input(z.object({
name: z.string(),
email: z.string().email(),
}))
.mutation(...)
// Frontend gets the types
trpc.users.create.mutate({
name: 'Alice', // TypeScript knows this is required
email: 'alice@example.com',
// @ts-error - 'age' doesn't exist
age: 30,
});

If you later need REST endpoints (for mobile apps, third parties, webhooks), add the REST adapter to your app:

src/index.ts
import { veloxApp, rest } from '@veloxts/velox';
import { collections } from './router.js';
const app = await veloxApp({ port: 3030 });
app.routes(
rest(collections, { prefix: '/api' }) // Add this line
);

REST uses naming conventions to infer HTTP methods, so you may need to rename procedures:

// Before (tRPC-style, short names)
list: procedure()...
get: procedure()...
create: procedure()...
// After (REST-style, includes resource name)
listUsers: procedure()... // → GET /api/users
getUser: procedure()... // → GET /api/users/:id
createUser: procedure()... // → POST /api/users

See REST Conventions for the full naming guide.