The goal for this chapter is to continue our TRPC configuration which we started in the previous chapter. In this chapter, the goal is to enable transformers on TRPC, add auth to our TRPC context, add a proper protected procedure, and we will also implement rate limiting. And that will pretty much wrap up everything we need in regards to our TRCP configuration and then we will be able to finally start adding some other entities to our database. We're going to start with a very simple entity later on called the categories. But let's not focus on that.
Let's focus on what's in front of us. Let's do the easy one first, enabling transformers on a TRPC. Let me just refresh my app to ensure everything loads nice and well and let's go inside of TRPC right here. Inside of my init method right here you will already see that I have this commented out transformer superjson. So let's uncomment that and let's go ahead and import super json from super json.
Now keep in mind that we don't yet have a super json. So let's go ahead and do a bun add super JSON. And you're going to see which version I'm using in a second. So I'm using super JSON at 2.2.2, like this. So let's go ahead and ensure that we have that here.
That's great. Now let's go inside of query-client, and let's do the same thing here. So import super JSON from super JSON, enable serialize data to use super JSON serialize, and enable deserialize data to use super JSON.deserialize. And in the server.dsx, I think we don't have to do anything. Let me just confirm in the client here.
Do we have any superjson here? We do have superjson here as well, and we also have an error here. So find the TRPC client here, HTTP batch link, so I'm inside of client.tsx, go ahead and uncomment this, remove this line, and now we also have to import super JSON from super JSON like this and now we should no longer have any errors. I'm just going to search throughout my code to ensure that there are no errors anywhere and nothing much should really change here. If I refresh now I should be getting no errors at all.
Great! So now that we have that let's go ahead and see what's next. What's next is to add Auth to trpc context. So right now let's go ahead and go in one of our routers here. If I go ahead, for example, and add console.log here, hello world, and if I refresh here, we should be able to see hello world right here on our server side.
So now I'm going to try and console log the currently logged in user. At the moment we can't really do that. Well technically we should be able to do it if we just destructure user id from await auth which you can import from clerk-next-js-server And if you mark this query as an asynchronous query, and then if you just add user ID in the console log and refresh, you can see that my user ID is null here. But if I go ahead and log in now with my email here, let's continue. Let's get redirected back, okay.
Let me try again. There we go. You can see that now I have a hello world and I have my user ID. So that's great. Some of you might already have some ideas how we can fetch the user from the database now and get the actual database record.
But it doesn't really make sense to do this every single time, because what we have to do then, if we want this to be a protected procedure, is check if there is no user ID and then throw new DRPC error like this, and give it a code of, this would be unauthorized or unauthenticated, right? So it doesn't make sense to do that every single time. So for now that's not something we're going to do. We can leave this, we can even remove the async. We don't need any of this.
Let's create a reusable way of doing this. In order to do that, we're going to go back inside of our init method here and we're going to revisit our createTRPC context. If you want to, you can learn more about the context and you can see how they give you an example of using NextAuth to get session inside of that context. I had a talk with one of the maintainers and creators of TRPC, and I asked them, how exactly, what exactly should I do in the context and they gave me a tip that I should not really do any database queries inside of here. So the context is something that will be available for every single API call that you do.
So we want to keep it as light as possible. In order for us to do that, the perfect method is the awaitAuth method from a ClerkNext.js server and then simply return ClerkUserId, userId. The reason we want to use this one is because the out helper returns the out object of the currently active user. Only works server side such as server components, route handler, server... Okay, I was hoping they're gonna give me a description of what I was hoping for here.
Basically, this doesn't do, as far as I know, any fetch, any fetches, right? It simply destructures the current JVT token or the current session, right? Which makes it a very lightweight method to call, as opposed to, for example, currentUser from Clerk Next.js, which returns the backend user object of the currently active user. It calls the fetch, as you can see right here. It calls this method, whereas out does not do that right here.
All right, so now that we have this, I also return this in this context under clerk user ID. So the reason I return it under ClerkUserID and not just UserID is because I want to be very explicit. My UserID right here inside of this context, whenever I see UserID, I will explicitly mean on this, my database UserID. But what I have here is my clerk user ID. So I want to make sure that all of my procedures will know that whatever I'm doing in the context is not the database user ID, it's just the clerk user ID.
Alright, so we have the context here and now what we have to do is we have to create a proper type for our context. So export type context will be awaited return type of type of create TRPC context like this. Now when you hover over context you can see that we have a potential Clark user ID inside. And now we're going to add that inside of this, initTRPC.context, pass in the context type like this, and then passing the create like that. There we go.
And now what we are going to do is the following. Let me just see, Can I now go inside of my app here? We have options, is context. Okay, I think that inside of options here, I think I can do console log now options, sorry, options. There we go, .context.clerkUserID, like that.
So that's what I wanted to see. So let's go ahead and just add from context, options, context.clerkUserID. I mean, since the moment we are getting proper types here, it probably means that it's working just fine. So let me go ahead and refresh and there we go. From context and we have the proper logged in user ID here.
Great! So that's one step better. We don't have to repeat our await out method, right? We only did it once, but how about we create another type of procedure which we can then use every time we know the user has to be authorized for that call. In order to do that we can export const protected procedure.
And what we're going to do is we're going to extend the base procedure like this and then we're going to use a middleware inside. So let's use asynchronous function isAlphid like this and it's going to have options inside. Let's go ahead and let's destructure context from the options. If we have a missing context clerk user id we we're simply going to throw new trpcError. Make sure you import this from trpcServer, from the package, not our instance of trpcServer, and pass in the code unauthorized like this.
And then what we can do inside of here is, well, we can, you know, end the call here. For example, we can just return options.next like this, and we can just spread the context inside, and user ID will be context clerk user ID. Well, actually, we don't have to do that because, okay, this doesn't make sense now, but I will explain why I'm doing it like this in a second. But already, nothing should really change right now. If you go inside of your router here, for example, and replace the base procedure with protected procedure now and remove the base procedure here, If you refresh, you shouldn't get any errors, everything should work fine.
But if I log out and refresh, now I should be getting 401. As you can see right here, something is going on, something is wrong. That is because I explicitly used protected procedure. But if I change it back to base procedure and refresh, there we go, no error at all. I just simply get a console log from context to be null.
Great, but let's go ahead and do one step further which is to actually always add the database record which we have because remember we have our webhook running. I'm running bunrun dev all which means that I have a proper webhook here which means that all of my users are synchronized. Let's just confirm that. If I go to Bonnex Drizzle Kit Studio here, local Drizzle.studio, and if I go inside of my users, I see my user right here. So I want this object to also be available in my protected procedure and that's what we're gonna do now.
So let's go inside of the init here and first of all we throw the error if this doesn't exist and now let's go ahead and let's get from await database which we can import from alias slash database like this. So we're going to do database.select from users, which we can import from database schema, where equals users.id context, the clerk user ID. I made a little mistake here. We're going to fix it in a moment. First of all, we need to import equals from drizzle ORM.
And second of all, this query will not work. Why? Well because users.id will never equal clerk user ID. Instead we will have the field user.clerkID. There we go.
Like that. And Drizzle will always return an array of items right so even if you do limit to one which is completely okay to expect here it will always return back an array of items right You can see that it's always an array. The reason they chose that is because that's how SQL works. SQL will always return an array. So no matter how you want to do it, you can limit it to one, but just make sure you know to...
Of course, if this has changed in the future, don't worry. Just hover over data and confirm. Is this an array? If it has a little array here, that means data is an array. So what I like to do is I just destructure the first item from here, the user, like that.
And then what I do is I just check again if there is no user in that case I'm going to throw a new error just the same one as above which basically means okay you are logged in but for some reason I cannot find you in the database and I cannot let you continue further with this request. Alright, and then what we can do here is we can spread the context and just simply pass our user here like this. There we go. So now inside of our app here, I'm gonna go ahead and use the protected procedure here this time. And now I'm going to check database user to be our options context dot user as you can see here.
So now I can get that user. Let's ensure that we are logged in so this query does not fail. Let me refresh. Let me scroll all the way down here and there we go. My database user is loaded here.
So that's how we are going to use the protected procedure. Right? We could have done this entire database call inside of the context. But remember, the context will run every single time for every single procedure that we do, regardless if we want that to be a public procedure or a private procedure. So that's why it's best to just destructure the JVT token, something that doesn't do any fetch calls, which in this case is a wait out.
In case you yourself are, you know, planning on changing this to NextOut, this would be GetSession for you. Basically, something that's not doing an API call. And then in your protected procedure, you can go ahead and do the API call. And here you can see exactly what we previously did, right? We created a type of context and we passed that here and then we were able to access options.context inside of a procedure.
Excellent! So we finished that. Let's see what else do we have to do. What we have to do next is we have to add rate limiting. So that's another interesting thing that we have to add and it's a great introduction to OpsDash because we will be using OpsDash in different places in this project.
The first place will be an easy introduction to OpsDash and a good practice for us to play more with this TRPC middlewares. We're first going to implement rate limiting so no one can spam our TRPC endpoints, and then later on, we're going to use Upstash for background jobs, for long running tasks in which we can completely rely on Upstash for retrying and keeping them in order. Those are going to be long-running AI tasks, which more often than not will time out in any non-edge or serverless function, right? They can take up to a minute or something like that. Or even hours, Opsdash workflow will be a perfect solution for that.
So that's why I want us to introduce to Redis very early on. So let's go ahead and finish that last thing. Because right now, you know, no matter how many times I refresh my app here, it will always load. I can never spam it enough. So let's go ahead to opstash.com and let's go ahead and create an account.
Once you're in here go ahead and create a database. I'm going to call this new tube. For the region I really don't know which one is best for you. You can just select the first one And let's go ahead and select next, select the free plan, right? And let's go ahead and see everything that we need right now.
So what I'm going to do is I'm going to click here and I'm going to click on the rate limit analytics And maybe I have to go to docs here. Let's go ahead and go together to learn how to set this up. So examples, next.js. Okay, this leads me to, this will lead me to the github file. Let me see if we can do it easier like this.
So we can start by just installing the packages which we need starting with the upstash redis package. So Let's go inside here and let's do bun-add at upstash slash redis at 1.34.3. That is the current latest version at the time of this tutorial. So I'm gonna add that. And then we're also going to do bun-add at upstash slash rate-limit.
And that is 2.0.5 like this. So let me quickly show you my package dot json. There we go. So upstash rate limit and upstash reddit. Redis, my apologies.
OK, now that we have those two, we have to add our environment variables. So you can find them here in your, you can just click, you know, on your project here, click on your database and you should be able to see your environment variables right here. So you can copy the ups-redis URL and ups-redis token, right this, you can just click copy, go inside of your .environment.local and add them here. So, ups-redis-url and ups-redis-rest-token, like this. Again, do not share this with anyone.
Now, let's go ahead and let's go inside of init.ts and what we're going to do here is we're going to import both Redis and RateLimit. So let's start by importing Redis from at upstash Redis and let's import RateLimit from at upstash RateLimit like this. And now what we are going to do is we're simply going to separate some space between base procedure and the protected procedure And let's add our Redis here to be new Redis with the URL to be process.environment and simply copy the Ops-Redis REST URL. And we're gonna have a token to be process.environment and then copy your Upstash Redis REST token. Copying is almost always safer than right typing it out because I've made so many typos when I did that.
And, you know, let's do this in a proper way. Let's go ahead and create a lib called redis.ts and let's do that instead. So new redis from upstash redis and then we can export this. Like this. So we don't have to define it here for no reason.
And we can do the same thing inside of our lib here or rate limit.ts. So rate limit.ts like that. Let's import rate limit from upstash rate limit. Let's import Redis from .slash Redis. And then let's do export const rate limit to be new rate limit.
Passing Redis as the first argument and then the limiter inside of here will basically be your definition of what is too many requests. For example, 10 requests within 10 seconds will cause a timeout or you can do 50 requests within 10 seconds will cause a timeout request. I believe that in Docs here, if we search for sliding window, you can perhaps learn a bit more about how it works. Let's see sliding window traffic protection right here. There we go.
So inside of here, you can learn a bit more about this limiter and how it works. For example, 10 requests within 10 seconds. And you can block this by anything. If you have an IP address, you can block by that. If you have a user agent, you can block by that.
I'm going to show you a very simple way to block by the currently logged in user ID. So let's go ahead and do that. Inside of the protected procedure, what we're going to do is the following. Once we ensure that we have this user inside of our database, what we're going to do is we're going to check if we will allow this user from making any more requests by using await rate limit, which you can import from lib rate limit. And yeah, we don't need this to import.
I think it makes no sense to have them defined inside of this file. So await rate limit dot limit and pass in the user dot ID like this. And in case success fails, we are simply going to throw new TRPC error code to many requests like this and now if you go ahead and try out and refresh a lot of times, so still not failing, not failing and now as you can see, I have TRPC error to many requests. And I believe that if you go inside of here, rate limit analytics, and we select new tube database here. I think that you should be, maybe not immediately, but I think you should be starting to see some requests here in a moment, or perhaps this will only appear in production.
Great, So you've learned how to build all of that. If you try one more time then it goes back to normal of course. Depending on you know your development style you might really really spam this refresh so feel free to increase this to 100 or you know you can completely just disable it if you think this will cause more problems than not you can just cancel it out like this and yeah I think that pretty much says everything we need to do in this chapter, right? Let's see. So let me get my pen here.
So we added the transformers to the RPC, we added ALT to the context, we added ALT procedure and we added rate limiting And we also got familiar with OPS-DASH and Redis. Great. So now let me do one more thing. And the only reason I'm doing this right now is because I did it in my project and I saw it in a couple of other projects, but I'm not exactly sure how much this benefits you. But inside of client.tsx, When it comes to TRPC create client, instead of this HTTP batch link after URL, what I did was I added asynchronous headers right here.
I defined the headers to be new headers like this. And then I did headers.set x-drpc-source to be nextjs-react. And I returned back the headers. Now I've found this to have some logging improvements, but not more than that. I just want to stay true to my source code.
If this ends up causing any problems for you, you can of course remove it because we just tested everything still works fine with or without it. But I think even the T3 stack has this line of code. So I think it will be helpful for logging and debugging. Right? So if you want to, you can add that.
And I think that this new headers is native. It's a fetch API, yes. MDN reference. So there was no import for the headers. Great, so I think that wraps up our chapter right now.
What we're gonna do next is we're gonna go ahead and add categories to our application. So we're going to load that carousel right here. You're going to see a picture in the next chapter. And we're going to organize our router a bit because right now we only have this entry point, underscore app, and we just started writing procedures here. And you can probably imagine that can be quite a mess if we just start putting everything here.
So we are going to restructure this just a little bit. And yeah, that's gonna be a good start into going back to our previous lesson, which was the hydration, right? So we are gonna come back to this prefetching thing, but this time, not with dummy data, but with actual categories, which we need to prefetch from our database. Great, great job.