Tl; Dr; I wanted to try tRPC with NextJS against REST, and it worked. If you just want the code, you can find the repo here.
A while ago I got curious and tried the T3 stack and it was amazing. The experience was just flawless. Prisma, NextJS, NextAuth, and Tailwind together, configured for me, just to start coding. After running through the documentation I had so many ideas as to what it could do for me.
But mostly I don’t get to have control of the whole stack, just the front end of it. 99% of the time I get some swagger documentation of all the endpoints available for me.
So using the T3 stack seemed wrong. I could just use NextJS. To be honest, I was kind of bummed. I loved tRPC. How I could use the procedures in my UI components.
I searched and I searched for some kind of documentation on the tRPC website for my use case. But no luck. So I gave it up.
Until today. My daughter was napping and once again the idea of tRPC popped up in my head. Well, I could at least try to mock something up and see how it could work. So I typed in the command in my PowerShell and was on my way.
npm create t3-app@latest
I picked everything except Prisma. On top of that, I quickly added Mantine for some sugar.
This is the application I built. A simple “e-commerce”. Well, at least it shows some products. The features and visual is not the point here.
I found a test API online that would work for this POC: Fake store API
My service layer
I never fetch stuff in my components. I typically create some services for endpoint access. In this application, it’s just some functions for fetching products.
export interface Product {
id: number;
title: string;
price: number;
category: string;
description: string;
image: string;
rating: Rating;
}
export interface Rating {
rate: number;
count: number;
}
export async function getAllProducts(params: {
limit?: string;
sort?: "asc" | "desc";
}): Promise<Product[]> {
const urlParams = new URLSearchParams({
limit: params.limit ? params.limit : "",
sort: params.sort ? params.sort : "asc",
}).toString();
const response = await fetch(
`https://fakestoreapi.com/products?${urlParams}`
);
const data = (await response.json()) as Product[];
return data;
}
First, some interfaces to describe the data (from the fakestoreapi). Then a function for getting the data.
tRPC Routers
In the repo, I have 3 routers: Cart, Product, and User
export const appRouter = createTRPCRouter({
cart: cartRouter,
product: productRouter,
user: usersRouter,
});
Let us look at the product router:
import { getAllProducts } from "@/services/products.service";
import { createTRPCRouter, publicProcedure } from "../trpc";
import { z } from "zod";
export const productRouter = createTRPCRouter({
getAllProducts: publicProcedure
.input(
z.object({
limit: z.string().optional(),
sort: z.enum(["asc", "desc"]).optional(),
})
)
.query(async ({ input }) => {
const products = await getAllProducts({
limit: input.limit,
sort: input.sort,
});
return products;
}),
});
In this router, I (for the demo) create a procedure called getAllProducts. It takes some optional input params and gets all the products from the service. This POC has no authentication involved so it’s just a public procedure. In the real world, there should probably be a protected procedure.
In all of the tRPC documentation and tutorials around the internet, the query/mutation is where you are supposed to do database operations (Prisma). I don’t really know if it is good practice to use it like this. I really wish this is ok because there is so much code that I don’t need to write because of this.
Normally I create my own custom React-query hooks that returns data from my services. I don’t have to do that with tRPC. I love it.
UI Component
This is where tRPC really does it for me. Look at this code:
const products = api.product.getAllProducts.useQuery({
limit: "8",
sort: "asc",
});
It is just beautiful. Not only the type safety but I didn’t have to write my own custom hook for using that query.
Anti corruption layer
If this is all done on the server I can also use the procedure as an anti corruption layer. The data we return should be what the UI want, not what the endpoint returns. What happens if backend suddenly deploys a breaking change to the API and it breaks the UI?
In the query we can (Not present in this demo) map the response to a ui model with fallback.
Conclussion
I like this approach. But I dont know if there are any fallbacks. Is this the way you can use tRPC? Is it something missing? Will it scale well in a large application? I guess it’s like everything else, I’ll have to try it, evaulate it and refactor if so.
Please leave a comment if there is maybe another approach for this or if there is another great, well respected library instead of tRPC for this cause.