How EM Routes are Defined in tRPC
Ok lets see how tRPC routes are created. in lib/api/server we can see a file called index.ts
import { categoryRouter } from './routers/category';
import { placeRouter } from './routers/place';
import { publicProcedure, router } from './trpc';
export const appRouter = router({
place: placeRouter,
category: categoryRouter,
});
// Export type router type signature,
// NOT the router itself.
export type AppRouter = typeof appRouter;
this defines a base app router which controls the api routes of trpc. These routes are located at the address /api/trpc/ A router can contain either
- Another router which is a collection of procedures
- A procedure which is the actual API endpoint
Routers can be nested inside other routers, letting you build a recursive tree. Each router is just an object created with router({...})
You can think of routers as folders which contain procedures. Procedures are the actual endpoints which we define our API with
Here is a graph of our routers and procedures for the EM codebase. So, for example, when i search place by bounds, the endpoint defined is
api/trpc/place.searchPlaceByBounds
And I can access the route in code as such.
const queryPlaces = api.place.searchPlacesByBounds.useQuery(
{
acsAttrs: params.acsAttrs,
categories: params.categories,
latBounds: latBounds || [0, 0],
lngBounds: lngBounds || [0, 0],
pageIndex: currentPageIndex,
pageSize: PAGE_SIZE,
},
{
enabled: !!latBounds && !!lngBounds,
placeholderData: keepPreviousData,
}
);
As aforementioned, tRPC exposes a useQuery wrapper for a tanstack hook. Read up on the documentation if you are unfamiliar with it.
On reflection - for the Thursday workshop i will do a deep dive into hooks and providers and react states
// src/server/routers/placeRouter.ts
import { router, publicProcedure } from '../trpc';
export const placeRouter = router({
searchPlacesByBounds: publicProcedure.query(async () => {
...
}),
summary: publicProcedure.query(async () => {
...
}),
});
Here is the code of the placeRouter showing how routers are defined and the subsequent procedures. Note that searchPlaceByBounds is a public procedure.
There are multiple types of procedures which we define in the codebase. A public procedure requires no authentication to access the API route. We can think of procedures as wrappers around an endpoint which adds functionality such as authentication or even debugging
We can define the behaviour of a procedure through certain exposed functions which augments the procedure.
For example:
.input(schema)Defines the expected shape of the data the procedure accepts.- Usually a Zod schema, which gives you both runtime validation and TypeScript inference.
.use(middleware)- Allows us to add functionality such as logging, debugging, or authentication checks before the procedure runs.
- A
protectedProcedureis just apublicProcedurewith an auth middleware attached.
For more on Zod: zod.dev
Procedures always end with a finaliser (.query, .mutation, or .subscription), which determines the type of operation:
There are three main types of procedures:
- Query -> returns a value based of an input
- Mutation -> changes a value based of an input
- Subscription -> continues stream of data based of input
.query(fn)/.mutation(fn)/.subscription(fn)- These are the finalisers — they determine what type of procedure this will be.
- After this call, the procedure is complete and cannot be further augmented.
So a usual procedure would be defined as such:
router({
someProcedure: publicProcedure
.input(...) // optional augmenters
.use(...) // optional augmenters
.query(...) // OR .mutation(...) OR .subscription(...)
})
Ok, now we are ready to create our own route!