Playground link: https://stackblitz.com/github/trpc/v10-playground?file=src%2Fserver%2Findex.ts,src%2Fclient.ts,src%2Fserver%2Frouters%2FpostRouter.ts&view=editor
tRPC V10 play
Draft of how a future tRPC-version could look like.
Do not try to run the project - there's no code implemented, only TypeScript ergonomics.
- Go to
src/server.ts
in CodeSandbox - Try adding/removing/changing queries and mutations.
- Go to
src/client.ts
and play around
- Run
yarn codegen
(modify./scripts/generate-big-f-router.ts
if you want) - Play with
./big/client.ts
and the connected routers to see how long it takes to get feedback - Potentially run
yarn tsc --noEmit --extendedDiagnostics --watch .big/client.ts
on the side
- More ergonomic API for creating procedures and building out your backend
- CMD+Click from a your frontend and jump straight into the backend procedure. This will work with
react-query
as well! - Enabling having a watchers-based structure - as you see, that
createRouter()
could easily be automatically generated from a file/folder structure. - Better scaling than current structure - the TypeScript server starts choking a bit when you get close to 100 procedures in your backend
Infer expected errors as well as data - unsure if this is useful yet or if it'll make it, but pretty sure it'll be nice to have.Skipped this because of it's complexity - it can still be added later.
type Context = {
user?: {
id: string;
memberships: {
organizationId: string;
}[];
};
};
const trpc = initTRPC<Context>();
const {
/**
* Builder object for creating procedures
*/
procedure,
/**
* Create reusable middlewares
*/
middleware,
/**
* Create a router
*/
router,
/**
* Merge Routers
*/
mergeRouters,
} = trpc;
export const appRouter = trpc.router({
queries: {
// [...]
},
mutations: {
// [...]
},
})
export const appRouter = trpc.router({
queries: {
// simple procedure without args avialable at postAll`
postList: procedure.resolve(() => postsDb),
}
});
Simplified to be more readable - see full implementation in https://github.com/trpc/v10-playground/blob/katt/procedure-chains/src/trpc/server/procedure.ts
interface ProcedureBuilder {
/**
* Add an input parser to the procedure.
*/
input(
schema: $TParser,
): ProcedureBuilder;
/**
* Add an output parser to the procedure.
*/
output(
schema: $TParser,
): ProcedureBuilder;
/**
* Add a middleware to the procedure.
*/
use(
fn: MiddlewareFunction<TParams, $TParams>,
): ProcedureBuilder
/**
* Extend the procedure with another procedure
*/
concat(
proc: ProcedureBuilder,
): ProcedureBuilder;
resolve(
resolver: (
opts: ResolveOptions<TParams>,
) => $TOutput,
): Procedure;
}
Note that I'll skip the
trpc.router({ queries: /*...*/})
below here
// get post by id or 404 if it's not found
const postById = procedure
.input(
z.object({
id: z.string(),
}),
)
.resolve(({ input }) => {
const post = postsDb.find((post) => post.id === input.id);
if (!post) {
throw new Error('NOT_FOUND');
}
return {
data: postsDb,
};
});
const whoami = procedure
.use((params) => {
if (!params.ctx.user) {
throw new Error('UNAUTHORIZED');
}
return params.next({
ctx: {
// User is now set on the ctx object
user: params.ctx.user,
},
});
})
.resolve(({ ctx }) => {
// `isAuthed()` will propagate new `ctx`
// `ctx.user` is now `NonNullable`
return `your id is ${ctx.user.id}`;
});
const isAuthed = trpc.middleware((params) => {
if (!params.ctx.user) {
throw new Error('zup');
}
return params.next({
ctx: {
user: params.ctx.user,
},
});
});
// Use in procedure:
const whoami = procedure
.use(isAuthed)
.resolve(({ ctx }) => {
// `isAuthed()` will propagate new `ctx`
// `ctx.user` is now `NonNullable`
return `your id is ${ctx.user.id}`;
});
const protectedProcedure = procedure.use(isAuthed);
export const appRouter = trpc.router({
queries: {
postList: protectedProcedure.resolve(() => postsDb),
postById: protectedProcedure
.input(
z.object({
id: z.string(),
}),
)
.resolve(({ input }) => {
const post = postsDb.find((post) => post.id === input.id);
if (!post) {
throw new Error('NOT_FOUND');
}
return {
data: postsDb,
};
})
}
})
procedure
.output(z.void())
// This will fail because we've explicitly said this procedure is `void`
.resolve(({ input }) => {
return'hello';
})
const postRouter = trpc.router({
queries: {
postList: protectedProcedure.resolve(() => postsDb),
postById: protectedProcedure
.input(
z.object({
id: z.string(),
}),
)
.resolve(({ input }) => {
const post = postsDb.find((post) => post.id === input.id);
if (!post) {
throw new Error('NOT_FOUND');
}
return {
data: postsDb,
};
})
}
})
const health = trpc.router({
query: {
healthz: trpc.resolve(() => 'I am alive')
}
})
export const appRouter = trpc.mergeRouters(
postRouter,
health
);
/**
* A reusable combination of an input + middleware that can be reused.
* Accepts a Zod-schema as a generic.
*/
function isPartOfOrg<
TSchema extends z.ZodObject<{ organizationId: z.ZodString }>,
>(schema: TSchema) {
return procedure.input(schema).use((params) => {
const { ctx, input } = params;
const { user } = ctx;
if (!user) {
throw new Error('UNAUTHORIZED');
}
if (
!user.memberships.some(
(membership) => membership.organizationId !== input.organizationId,
)
) {
throw new Error('FORBIDDEN');
}
return params.next({
ctx: {
user,
},
});
});
}
const editOrganization = procedure
.concat(
isPartOfOrg(
z.object({
organizationId: z.string(),
data: z.object({
name: z.string(),
}),
}),
),
)
.resolve(({ ctx, input }) => {
// - User is guaranteed to be part of the organization queried
// - `input` is of type:
// {
// data: {
// name: string;
// };
// organizationId: string;
// }
// [.... insert logic here]
});