Skip to content

tRPC Bridge

Server actions can call your database directly, but then you lose the validation, authorization, and type safety that procedures provide. The tRPC bridge lets server actions invoke your existing procedures with full type inference, so your business logic stays in one place regardless of whether it’s called from REST, tRPC, or a server action.

app/actions/users.ts
'use server';
import { executeProcedureDirectly } from '@veloxts/web/server';
import { userProcedures } from '@/api/procedures/users';
export async function getUsers() {
return executeProcedureDirectly(userProcedures.procedures.listUsers, {});
}
export async function createUser(input: CreateUserInput) {
return executeProcedureDirectly(userProcedures.procedures.createUser, input);
}
'use server';
import { validated } from '@veloxts/web/server';
import { executeProcedureDirectly } from '@veloxts/web/server';
import { CreateUserSchema } from '@/api/schemas/user';
import { userProcedures } from '@/api/procedures/users';
export const createUser = validated(CreateUserSchema, async (input) => {
return executeProcedureDirectly(userProcedures.procedures.createUser, input);
});
app/pages/users/index.tsx
import { getUsers } from '@/app/actions/users';
export default async function UsersPage() {
const users = await getUsers();
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}

Types flow from procedure definitions:

// Procedure defines types via Resource API
const createUser = procedure()
.input(CreateUserSchema)
.mutation(async ({ input, ctx }) => {
const user = await ctx.db.user.create({ data: input });
return resource(user, UserSchema.authenticated);
});
// Action gets same types
export const createUser = validated(CreateUserSchema, async (input) => {
// input is typed: CreateUserInput
const { db } = await import('@/api/database');
const user = await db.user.create({ data: input });
return resource(user, UserSchema.authenticated);
// return is typed based on projection
});