In this chapter we're going to focus on setting up the TRPC, our data access layer. So let's go ahead and do a quick reminder of what we did in the previous chapter. If I go inside of my app folder page.tsx, the last thing I added here was a direct prisma call so I can do this because this is a server component which can essentially have the same functionality as an API route in a sense that it can directly query the database And while this is super fun to demonstrate, it's not exactly the safest thing to do. Not because server components are inherently unsafe, but because raw access like this is unsafe in any type of server instance, be that an ABI route or a server component. Because of that, we're going to implement a data access layer.
This data access layer will be used to, for example, create a protected procedure so that only logged in users will be able to query this database call. Now there are different ways you can create a data access layer of course. I personally prefer TRPC. Many times that I implement TRPC I get asked why not server actions. I have nothing against server actions.
I just feel more confident writing this extremely large projects with TRPC. I also feel more confident inviting other collaborators if this project uses TRPC. And I also feel it is more maintainable long term if we use TRPC. The initial complexity for setting up TRPC versus server actions is a very big difference. Yes, TRPC is more complex in the beginning, but for me it is definitely worth it.
Some of this might just be my personal opinion, but I also do hold almost a decade in this industry and I just feel like TRPC is at the moment more mature than server actions. But of course to each their own. Some of you might be way more experienced with server actions than I am and might have found that it serves the same purpose. That's completely fine. I'm just trying to elaborate on why I'm choosing TRPC in this case.
So let's go ahead and set up TRPC so that we can modify this from directly calling a Prisma instance through a data access layer. So you can use the link on the screen to visit this page right here and now I'm going to teach you how to find the documentation that we need. So let's go ahead and go inside of the documentation here and let me just expand this and zoom out a bit so it switches to desktop mode And in here you can see we have the backend usage, we have the client usage. Go inside of client usage and find 10-stack react query with a star and then select server components. This is the guide that we are going to follow.
So first things first, we have to install all of these packages here. So now I'm going to go ahead and do that. So it's a trpc server client 10 stack react query and then individually 10 stack react query and then Zod client only and server only. So I'm going to install these packages and after that I'm going to show you the exact versions of these packages because they are quite important and if there are any breaking changes I want you to be aware of them. So let me go ahead and open my changes here.
Here I have my package.json. So let's see. I would recommend that you open package.json as well just to see if you have any large major version differences here. So my 10 stack version is version 5. My TRPC is version 11.
And my TRPC... Okay, yes, so every instance of my TRPC is version 11.6.0. So I assume all of yours will be a single version as well. My 10-stack React query is version 5. My client only is 0.0.1 and server only is 0.0.1.
I can't exactly tell you if you have to use the exact versions as me. I think it will work just fine, even if you use some newer versions. I would pay attention if you are like on version seven of 10 stack React query and like on version 15 of the RPC. Well, yeah, then maybe you will have some breaking changes. So at that point, maybe it's better for you to downgrade to my exact versions and run npm install again.
But if you are around the same version, I'm pretty sure you'll be fine. By the end of this chapter, you will know whether you are able to do everything the same as me or not. But just here's my exact versions in case you are interested. All right. So once we have established our packages, let's see what else we have to do now.
So initialize your tRPC backend in tRPC forward slash init dot ts. So inside of source folder, go ahead and create tRPC. And inside of here, let's create init dot ts. Like so. Let's go ahead and see the sample back end here.
So I'm going to copy it and I'm going to paste it here. Let's go ahead and review it quickly. So init trpc from trpc server, cache from react, we create a mock context which is pretending to be some kind of ALF session which gives us the user ID. In here we created the initial trpc instance with no settings inside. And then we create the actual router, we create the caller factory, and we create the base procedure.
We're going to explore what each of these are a bit later, but for now it's important that this is your init file. After the init file, let's go ahead and create our factory of routers, I guess you could call it. So inside of the trpc folder, create a new folder called routers and inside of here create a new folder underscore my apologies it's not going to be a folder it's going to be a file underscore app dot ts and inside paste the code from here. Again let's preview it. So what's going on here is we import Zod for some type safety.
We import the base procedure and create a TRPC router from the init function. So now already you can understand what these are. So createTRPCRouter is used to create new routers, and base procedure is used to, well, create procedures. Let me try to draw some parallels with an API route here to help you understand if it's your first time. A base procedure would be basically an API call.
So either a, for example, .query would be a getAPICall. If I did .mutation, this would be a postAPICall. Well, or put or patch, right? You get the gist. We're just using an RPC way of building these API routes.
We still haven't touched the create caller factor because it's a little bit complicated to explain. But for now, I hope you understand, okay, so the DRPC router is used to creating routers. I'm not sure what parallel to drive exactly with an API route, but trust me, when you see the context in which we are going to use this function, it's gonna be very clear what createDRPC router is used for. Base procedure is very clear. And later when we need to create, for example, a protected procedure, we will have protected procedure, which will simply extend the base procedure.
And then in here, we're going to create a middleware which will return false or true depending if the user is logged in or not. And that way, if user wants to query hello, we're gonna use the protected procedure and then it's going to throw errors if the user is not logged in so we don't have to do it every time. So that's what procedures are in a very, very short explanation. Great. So make sure you have this app folder with a very basic app router and a hello based procedure with a query.
Make sure you didn't modify this to mutation and forgot to turn it back once we have done that let's go ahead and let's establish the app folder api folder trpc and then a dynamic TRPC folder with a route.ts inside and copy this in here. So okay, this is very, very Next.js specific. Let's go inside of app folder, let's go ahead and create API folder inside, then Let's create trpc and inside of that let's go ahead and create trpc again but this time inside of square brackets. And finally, route.ts. So route.ts is a reserved file name the same way page.tsx is, Except page is used to create client routing, whereas route is used to create server routing.
So yes, Next.js has built-in API and you write it like this. So this would be an equivalent of going to localhost 3000 forward slash API forward slash the RPC. And what this is, is basically a dynamic route. Basically anything can go in here. It's like a variable, like a param, right?
So I can go forward slash one, two, three and it would be a correct hit. I hope that kind of makes it clear. It's basically allowing tRPC to send this RPC procedures through our API call. All right, so if you paste this from the content here, you will get some errors. Luckily, they are very easy to fix.
So they are using this, I'm not sure, wavy little sign as the import alias. And we are using the at sign. So just change this to the at sign, and this too, and that's it. In here, it's important that the endpoint is correct forward slash API forward slash trpc which is exactly what we have established here. So there isn't too much to talk about this route here Basically it creates a handler and it exports it as the get route and as the post route.
And it uses the app router from tRPC routers app that we have created. It also assigns the context here which for now is not really useful but it allows this API call to have it here. So just make sure you have this set in this exact structure, make sure there are no typos here and make sure you are doing that inside of the app folder of course. Perfect. Once we have established this let's see what else we have to do.
So now we have to create trpc query-client.ts. Let's go ahead and let's do that. So inside of source, trpc, let's create query-client.ts and inside of here we're going to paste this code let's go ahead and see what we have to do So we import from tanstack react query, default should hydrate query, dehydrate query and the query client. What you're seeing here is basically just a tanstack query configuration that works well with trpc. I have never ever modified this, it just works fine as it is.
It's basically a singleton 10 stack query instance. One error that we have here is the super json import. For now we can comment this out, we don't have to worry about it for now. We are going to install superjson later and then add these serializers and transformers and I will explain why. But for now, yes, that's it for trpc query client.
If you are interested in about the settings you can actually read from the documentation here. Now let's actually create a trpc client for client components. So what is this? This is a very large file. Let's go ahead and add it first.
So trpc forward slash client dot tsx. Trpc client dot tsx. It's important to have a tsx extension because there will be some jsx inside of here this one right So what's going on in this file here? First of all, we mark it as useClient. This automatically means that this is now a client component.
Then we go ahead and we add our 10-stack query client here. We have some helper utils to get the URL of this project and once we get that URL we can target that TRPC endpoint in the API routes that we have created and then we create the provider. In here as you can see it's very clear what they're doing, they are initializing get query client in a way that it works in this whole environment. It is a little bit complicated, but the good news is you will never have to modify this. So As you've copied it from their documentation, it will work.
And basically, we're going to use this to wrap our entire project within both the RPC provider and query client provider. So think of it as a provider for some context, right? We are just going to wrap our app around this entire thing. So that's what we need this for. And I believe that once we do that, we should immediately add it to our app layout.tsx.
So let's go ahead and do that so we are exporting let's see what's the name of this component trpc react provider let's go inside of the source folder app folder layout folder here and let's go inside of body and around the children let's add trpc react provider from trpc client. Like that. From at trpc client. The exact file we have just added. So basically this will now allow us to use trpc and react query or more specifically 10 stack query within our entire application.
Perfect. Once we've done that, let's create a trpc caller for server components. So I'm going to go ahead inside of trpc and I'm going to create server.nottsx. Actually yes, it is. Let's see.
Let me copy the file and let me paste it here. Let me just expand this a little bit. So we are importing server only. So it cannot be imported from the client for security reasons. So basically yes, when you add this import server only and if you try to import this file in a client component it will throw an error in development node and it will prevent any builds from happening so you will never be able to leak any secrets or any access to the server with this.
That's why this is very important. And now this is actually not needed. So if your router is on a separate server, pass the client. So just remove this. It's actually much simpler for us as you can see right here.
So what we're doing here is we're creating a caller instance of TRPC for the server component. This will allow us to call the TRPC data access layer through a server component. So you already probably know that since we added 10-stack query, on the client side we're going to be using useQuery and useMutation and then inside, instead of writing the usual string that targets our API, we are going to call the TRPC procedure. But how would we do that on a server component? Well, using this file right here.
And this is one of the advantages of TRPC, because I worked with other RPCs, like Hono RPC, And Hono did not have the ability to do that. Well, it did, but the out session would get lost. One of the cool things about the TRPC's server caller is that it can preserve the out session within this. So it's a much better solution in my opinion. We will almost never use it, but it's very cool to have it and you will see.
I will show you by the end of this chapter a few ways you can use DRPC. So maybe it will give you an idea for your own project. Just make sure you have added that file here. Perfect. And looks like that is it.
I think that it is. Okay. So, this is what I want to do now. I want to go inside of source. I want to go inside of the RPC routers app.
So I'm going to modify this hello base procedure to be for example get users. Again just a normal base procedure and we don't need the input at all. We can just do query here and instead of return here I'm going to do now await, actually I can just directly do my Prisma from libdb database.user.findMany. I don't need the options at all. So a super simple get users procedure as you can see right here.
We can remove the Zot import from here. Like this. So now, for example, let's go inside of source, app folder, page.tsx. In here they give us a bit of a complicated way of doing this I want to get to that later. Let me try and find a simple example first.
For example, getting data in a server component. So far we've done this directly using await prisma. But now we have abstracted this within our router, right? Within the get users procedure. So now we have to find a way to query trpc.
So here's what we should do. So in here, they tell us that in server.tsx, we should have this caller or that we should create it. So let's just quickly see inside of trpc, server.tsx. So I have the get query client, I have trpc, but I don't have the caller. So I just added it.
Export const caller is app-router.createCaller and execute it and paste the create-trpc context inside so this exact line that you can see here. And once I have that caller instead of importing Prisma in this server component I can now import caller directly from TRPC server. And then users in this way will be caller.getUsers. And as you can see, the type safety works exactly the same way. So yes, type safety is now preserved through our Prisma schema, through our data access layer, all the way through our component.
That's the power of combining Prisma, tRPC, and TypeScript. And if you go ahead and try this now, so for two things, let's do npm run dev in one. Actually I already have it running. So npm run dev in one and in another one I'm going to do npx prisma studio. So studio will open on localhost 5555 so I'm going to click add record email Antonio at mail.com name will be Antonio and I will click just save one change.
Basically we already did this before I'm just adding a new item here. And now let's go to localhost 3000 here and if we've done everything correctly it works just as it did before but this is a much more secure way of doing that and I know it seems like we did so much for so little but this is way more scalable trust me because this is a super simple example. We're not passing any inputs. We're not passing any params. We're not doing anything complicated.
We're just doing a very simple fetch from the database. But you will see as this project grows, you will be so thankful that we added the RPC so early so that we can leverage it through the rest of the project in a super type-safe way that's maintainable, that's easy to understand, that you can come back in six months from now and completely understand what you're doing. I find this way more understandable than server actions or something like that. That could be just my opinion. It could be that I'm just wired to understand this, that I like this more.
Alright, so now that we have this, we have solved this super simple example of fetching this in a server component, right? This basically, we repeated the example that we had. But what if this was a client component? So what if I had used client at the top? Because we are definitely bound to have some client components in this project.
Well, in this case, as you can see, we get an error. And thankfully, this caller is throwing an error here because we imported server only. So it would be a very big mistake if we imported that caller on the client because then the client would be able to query the database. That's horrible, right? Well, it wouldn't be able to query, but you never know what secrets might leak from that.
So we have to modify this a little bit. First, let's remove this. Let's remove the import and let's remove this entirely. Now, once we save, of course, we're going to get the error for the users. So let's go ahead and do the proper client way of fetching from trpc.
So const trpc will be use trpc from trpc client. And then in here, let's go ahead and let's do data let's map it to users and let me just expand this a little bit use query from tanstack react query and now we do trpc.getUsers.queryOptions like this and you can see that we have now achieved the same thing the only difference is since this is a client component it doesn't load as fast as the server one. So you can see that we have a small blink here. Right? So I'm just going to save this for now and I'm going to quickly revert to the server component just so you can see the difference.
You can see how server component is much faster, right? I'm refreshing right now and it's instant. But a client component, which is what we're used to, is a little bit slower. So you now know how to do it when you have a server component. You know how to do it when you have a client component.
But there is a third option. And the third option is how we're going to use tRPC through this entire project. And that's actually this example that they have added here, which I think is the most complex example. So that's why I didn't want to show it to you first. So let's go ahead and do that.
Let me just try and follow the proper example. Okay, so they're doing it with this hydration boundary. Okay, yeah. So let's go ahead and do it. The way this will work is as follows.
Page would be left as a server component and we would create a new file called, For example, client, which would be a client component. Since this is not a reserved file name, it's just a random file which is a component, we can do a named export instead of a default one. Client component. So let's go back inside of the page here and let's revert this back to a asynchronous component. So what people usually do, you know, in order to fetch, let me just revert this, in order to pass data from a server component to a client component is they would render the client component and they would pass in the users.
Right? So, let's go ahead and just add users. I don't know, it's a record string, any, and an array. Let's get the users, client component, JSON stringify users. So that's one way of passing data from your server component to client one.
And you can see it works pretty fast, right? So we are technically leveraging the speed of server components to load, and then we are handing off to a client component which can do all the hooks. But there is a problem with this solution and the problem is there is no way for this client component to have any other state about this query. It only has the final data. It also cannot refetch.
It cannot do pagination. It cannot do a lot of things that you would have to do in a series project. Because of that, that's not how we do this. We do it in a different way. So in the server component here I'm going to go ahead and leave this import and I'm just going to modify this a little bit.
So I'm going to import get query client from the RPC server and I'm going to get the query client here to be getQueryClient like that and then I'm going to wrap my client here in a hydration boundary from 10-stack react query. I'm going to remove the client users prop entirely because we don't need it. And I'm going to remove the type from it as well. And in here I'm going to pass the state. So state is going to be dehydrate from tanstack react query and pass in the query client.
Like this. So what we've done now is we are creating a boundary between a server component and a client component. So what I'm going to do now is I'm going to go ahead and prefetch this by doing void query client dot prefetch query and then I'm going to pass tRPC which I can import from the server. So make sure you have imported it here. So the difference is in the client component you would get the tRPC from use tRPC.
In server components you can just import it from the server library. So tRPC.getUsers.queryOptions So what this is doing is it is leveraging the speed of a server component by instantly starting to pre-fetch this. And in the client component what we would have to do then is the following. We would get our data, which is users, in two ways. First one is by doing useQuery, but since we are prefetching here, We can almost expect it by using useSuspenseQuery.
Let's get drpc, use drpc from the client, and pass in drpc.getUser'sQueryOptions. And then, let's go ahead and try it out. You can see how now when I'm refreshing, it is extremely fast. It is as fast as the first time I tried doing this with just a caller in a server component. But this is a much more advanced case now.
Because we are no longer calling any data, server components has no data in this moment. Instead, what we're doing is we are leveraging tRPC's prefetch and we are populating 10-stack query's client state with it. So we are leveraging server component to start fetching very fast because it will appear faster than the client component. We have already demonstrated this. You remember that little blink that happened when we switched to a client component.
Well, that blink is no longer happening even though we are completely doing this on the client, right? But the difference is that now this client component will be populated from the server component. And then you can also leverage things like suspense here from React. Fallback And let's add loading. And this way we are going to handle loading states and later on error states.
So this is the structure that we are going to use. We are going to leverage the power of speed components, but we're going to hand off to a client component which is familiar for us to use. I'm sure that all of us are more familiar with hooks rather than keeping track of what we should do in a server component, what we can do, what we can't do, and doing all kinds of tricks to make it work. Instead, let's just use the power of server components for speed, and let's hand it off to a client component and then use it in a familiar way. So this is a much better way than just passing the data through props to a client component because this way we can actually manipulate this data.
So I now have useSuspenseQuery here. I can get the errors, I can get the loading state, I can refetch, I can change the settings to refresh on window focus. I can do polling now. I can invalidate query data. I can do all of those things now which I previously could have only done in a server component or maybe couldn't because server components are limited when it comes to hooks and all of those things.
But now if you know 10-stack React query, you know the rest of the project, right? This is nothing new to you. The only new thing is how to prefetch this and put it to a client component. So that's what I wanted to demonstrate. That's why we are doing this combination.
This is why I prefer tRPC. It is complicated and maybe you don't even realize if we achieved anything impressive now. But I promise you by the end of this tutorial and everything that we will build, you will be very happy that you added tRPC this early and that you learned how to do this. I know this is complicated, but you will see. The more we build with this exact structure, the simpler and more understandable will it be for you.
So we set up the PRPC, we created a procedure with Prisma API, we explored the PRPC client side, server side, and finally the prefetch, which leverages the power of server and the familiarity of the client. Now let's create a new branch and push this. So this is called a 03-TRPC setup. So I'm going to go ahead and click down here. Create new branch.
03-TRPC setup. Once I'm in a new branch, you can see I have 11 unstaged changes. Package lock, package. Client layout page, route, client in source-trpc, init in source-trpc, query-client in source-trpc, server in trpc, and underscore app in trpc routers. So I'm going to stage all of those changes and I'm going to make a commit 03 trpc setup commit and let's publish the branch and now that we've published the branch let's go ahead and open the pull request.
So let's just click compare and pull request same as we did last time or if you're creating a pull request manually the base is main and the compare is your new branch. Let's go ahead and create a pull request and then let's review it. And here we have the summary by CodeRabbit. Let's go ahead and see what it thinks. So, new features.
We introduced server-backed data fetching and a client view that displays the user data. We added suspense-based loading states for smoother user experience. Performance. We created faster initial load using server-side prefetching and client hydration. We improved responsiveness with cached queries and batched requests.
Reliability. We created more resilient data fetching with automatic caching and revalidation. We also added dependencies to support RPC based APIs and react query integration. As in the previous chapter, we have a cohort or file by file summary of changes that we did. But what I find more interesting here is, of course, these amazing diagrams here.
So let's go ahead. Let me just try and see it like this. There we go. So what's going on here? I actually think we should start with the bottom one instead because the bottom one here is the server side, right?
So once the next JS page on the server hits the root page. What we do here is we get the query client and using that query client we are prefetching query get users. Once that is prefetched what it does is it populates the cache of the query client and with that new cache we then render the client with of course dehydrate react query so this cache is properly populated there and then the client component through our hydration boundary ends up having that hydrated state. And we can actually stop here and we can now go back in the upper one to see what's going on. Let's see.
So user browser this time, right? Render after hydration hits the client component. We have this entire flow, but as you can see, not in our case, because we get a cache hit. So we can skip this entire usual flow that happens on the front-end side and we just read the data from cache. That's why it's so fast.
But even if that cache fails for whatever reason or the cache is invalid, you know, maybe enough time has passed on the client component that we should refetch the data. In that case we do the usual fetch get users, do an HTTP batch request, route to app router, get users, prisma, users, find many and bring back the result to cache. But what I'm trying to say is that we skipped this entire part by having the prefetch functionality which automatically does a cache hit on the client component which means that we have data from cache. And I'm super impressed how CodeRabbit entirely understood what we did here. This is such a complex topic and CodeRabbit perfectly understood what we just created.
And in here, it also reviewed our code in detail, But since we are going to modify this in the next chapters, we don't really have to take it to heart. Yes, these are not exactly the most ready snippets yet. Some of them have to stay the same way because we copied them from the documentation, but some of them will be modified. For example, in here, it says that the context is useless. And it is because we never actually inferred the context type.
We are gonna do that, but later, we don't need it now. In here also, it notices that we are doing prisma user find many. So it modifies our procedure to have a limit, to have a cursor, it basically added the entire pagination for us, or at least to limit it to something so it doesn't give us too many data back here. So that's how useful CodeRabbit will be in this tutorial. I'm super impressed by how easily it can create these diagrams which help us explain what's going on with our code, which is especially useful in this very complex scenario.
So I'm so happy that I managed to explain once again how this prefetch is working and why it's so useful for us to populate that cache because then once we reach the client side, we are just using the cache hit and we can skip this entire complicated and long query instead. That's why it's so fast to leverage both server components and client components together because you get the both of best worlds. You get automatic caching and revalidation of the client components, but you get faster initial site load with server side components. That's what we are doing here. Amazing.
So I'm not going to modify anything right now. I'm just going to merge this as it is because I am satisfied with it. Once we have merged it, I don't delete my branches. I like to keep them. So in my source code here, I can have each branch individually.
But once you have changed, let's go into main branch and make sure to always click on synchronize changes and okay, and then go ahead inside of your source control here, click on graph and in here you should see a detachment for TRPC setup and then merging that back in. And if you go inside of source and see the TRPC folder on your main branch, it means you did everything correctly. I believe that marks the end of this chapter. So let's go ahead and confirm. That's it.
We created a new branch, BR, and reviewed and merged. Amazing, amazing job on handling this complicated chapter. I hope I kind of managed to explain why we did it and the long-term benefits of it and see you in the next chapter.