{
  "video_id": "ED2H_y6dmC8",
  "title": "Build and Deploy an N8N & Zapier Clone | Next.js 15, React, Better Auth, Polar | Full Course 2025",
  "url": "https://www.youtube.com/watch?v=ED2H_y6dmC8",
  "transcript": "Every tutorial out there teaches you how to use N8N or Zapier, but what if you could build your own instead? In this tutorial, we're building NodeBase, a complete workflow automation platform from scratch, drag-and-drop canvas, real-time execution, multiple integrations ready to go. Plus, we're building the full software as a service layer, payments, subscriptions, free tier, and paywalls. By the end of this tutorial, you'll have a production-ready platform that you can actually ship and charge for. The heart of any automation platform is the canvas. We'll build a clean interface with trigger nodes and execution nodes. You'll learn how to create webhook triggers. Google Form submissions Stripe event listeners and even manual triggers For execution nodes we'll build AI integrations such as OpenAI, Cloud and Gemini plus some messaging platforms like Discord and Slack and the generic HTTP request node But here's the key Once you understand how these work you will be able to add any integration you want Airtable, Notion, SendGrid. The only limit is your imagination. Let's build something practical. We'll start with a Google Form trigger, perfect for collecting customer feedback or survey responses. When someone submits the form, we'll send that data to OpenAI to analyze and summarize the responses. Then, we'll take that summary and send it to both Discord and Slack, notifying your entire team instantly. Each node has a configuration panel where you map data from previous steps using simple template syntax. The data flows from one node to the next and you control exactly how it transforms along the way. Now here's where it gets visually satisfying. While the workflow is executing, each node will display its current status. You see exactly which node is working, when it completes, and if anything fails. For example, the Google Form triggers. OpenAI starts thinking and completes successfully. Then Slack fires off and succeeds. But Discord, it lights up and fails with a red error state. You can see exactly which node has the problem, and you caught it in real time. Everything updates through web sockets, which means no polling and no page refreshes. You're watching your data flow through the system as it actually happens. All of this is powered by Ingest. It can handle background job execution, manage retries if something fails, and gives us real-time pub-sub messaging for those live updates you just saw. Your workflows run reliably in the background while the UI stays responsive and shows exactly what's happening. For our database layer, we're using Prisma as our ORM coupled with Neon as our Postgres provider. This gives you type-safe database queries in a modern stack. Authentication is handled by better out, which is quickly becoming the new standard for out in the Next.js world. It comes with tons of providers out of the box and makes complex outflows actually manageable. For payments and subscriptions, we're using Polar, which gives you free tier, paid plans, and usage-based billing. Everything you need to monetize your platform. This is more than just a coding tutorial. It's a real-world development workflow. Each chapter ends with a new branch and a pull request. We're using CodeRabbit for AI-powered pull request reviews. So you'll see how to structure your code, handle reviews, and merge features just like you would in production. It's a complete GitHub workflow tutorial built right in. In order to make this project production ready, we are integrating Sentry for error tracking, logging, and session replays. But the real power is in their AI agent monitoring. Every time an LLM call is triggered, we get extremely detailed information. Which model was used, exact token counts in and out, cost per request, execution duration, and the actual response output. This way, you can trace every single AI interaction in your application with complete visibility. So let's recap the full tech stack. Next.js and TypeScript for the foundation. Prisma and Neon for our database BetterAuth for authentication Polar for payments and subscriptions Ingest powering our workflow executions with real-time updates React Flow for the visual canvas OpenAI, Cloud or Gemini for AI integrations CodeRabbit for our GitHub workflow and Sentry making it all production ready This isn't just a workflow automation tutorial. This is a complete playbook for building and shipping a real SaaS product. And now, let's get started. Before we dive in, using the link on the screen, you can get three months of Sentry Team completely for free. We'll be using their AI monitoring to track all our LLM calls throughout this build. If that sounds useful for your project, feel free to grab the deal. And now, let's build. Let's get started by creating a Next.js application. In order to do that, we have to meet specific system requirements, as outlined on their installation page. As you can see, Next.js requires Node.js with a minimum version of 18.18. In order to confirm what version you have, head inside of your terminal and run the command node-version. If you get an error or if you get a version lower than 18.18, it is time to upgrade or install. You can do that by clicking on Node.js link within the Next.js installation page or simply googling Node.js download. As you can see, the latest long-term support version is 22.20.0, and this is the easiest way to set up Node.js on your system. Once you've done that, go ahead and run this command again. I would highly recommend that you are on the same Node version as me. And while you're here, also double-check that you have npm and npx. If you're using the same node version as me, you will definitely have these installed as well once you install Node.js. Keep in mind that versions of npm and npx can be different depending on what operating system you are on. So it doesn't matter that they are exactly the same as mine. They can be different versions. What's important is the node version and that you don't get errors for running any of these three commands. Once you have confirmed you have that, let's go ahead and install Next.js. As you can see, we have a command which we can use to bootstrap creating a Next.js application, but we are going to slightly modify it. As you can see, they are using the at latest tag, which is perfectly fine. But in my case, and more specifically in your case, I don't know when you are watching this tutorial. For that reason, I want to make this tutorial in a way that you can follow regardless of when you are watching this. So the exact latest version at the time of me making this tutorial is 15.5.4. So I would highly recommend that you write 15.5.4. And this way, you will be using the exact same stable version that I am using. and you won't have to tackle with any breaking changes that might have been added. So yes, don't worry if at the time that you are watching Next.js is on a higher version, right? The way I'm building this tutorial is so that it works for 15.5.4. If you want to upgrade later, I would recommend that you do that after you finish the tutorial. It will make it much more easier for you to follow along. after you've done that let's go ahead and give this project a name so I'm going to call this node base the name of our project let's go ahead and install this create next app and let's select yes for TypeScript we can select any linter you prefer it will not change the project whatsoever for example I'm going to choose biome for Tailwind CSS make sure you select yes make sure you select yes for the source directory as well. In case this is your first time seeing this install wizard, you can use the arrow keys to choose between yes or no. So make sure you select yes. Same for the app router, same for Turbo Pack, and select no for customizing the import alias. And now let's just wait for this to install. Once the project is installed, you will see a success message like this. So then go ahead and immediately change directory inside of your newly installed project, NodeBase. Now let's go ahead and do a simple ls just to confirm that we are in the correct repository. You should have a readme, a biome file, node modules, public, source, and some package config files. Perfect. Now that we have set that up inside of our terminal, let's also set up inside of our IDE. I'm going to be using Visual Studio Code, but you can use any editor you prefer. So let's go ahead and open this project. I'm going to go ahead and select NodeBase and click open. If you get a prompt like this, let me show you exactly how it looks like, you can select yes. Now let's go ahead and just quickly review our code. So our file structure looks like the following as I already mentioned by running the ls command we have some config files we have the node modules folder public and the source folder What important is that you have the same structure as I do which is almost 100 bound to happen if you used the exact same version as me So that's why I'm telling you to use the same version so you don't have any surprises here. You should have the source folder, you should have the app folder and inside you should have a favicon globals layout and page so let's go ahead and run this project now in order to run our project all we have to do is go inside of our terminal and do npm run dev since we selected turbo pack we will have much faster developer builds and you will see dash dash turbo pack right here and then you will see where to access your page on localhost 3000 so let's go ahead and go to localhost 3000 and once you refresh you should see next js here depending on if your system is in light mode or dark mode you obviously might see this as a white background so let's go ahead and try and change something i'm going to go inside of page.tsx within the app folder here and i'm going to remove everything and instead i'm just going to write const page like this return let's add a simple div hello world and export default page and let's save this file as you can see i just have a plain hello world text let me zoom in a bit so you can see it right here so what happened why did this work how did i know which file to modify what happened here is that i went inside of the app folder which is basically uh the router of the Next.js application and I selected page.tsx which you can imagine as the root file of this project. Page is a reserved file name within Next.js so if this was called something else it would not work it would not be recognized as a well page. The second important thing for you to know when working within Next.js is beside the reserved file name which is page you also have to do a default export. For example, if I forgot to do a default export and instead did this, you will see that I will encounter an error. The default export is not a React component in forward slash page. So that's why I did export default. So make sure you do that as well whenever you are creating pages. Perfect. Now that we have this, let's go ahead and quickly test if Tailwind is working within our project. So I'm going to go ahead and give this div a class name, text red 500, and it immediately turns red. So now you've probably noticed that I have this cool little color indicator here. And not only that, when I hover over classes, I can see the exact underlying CSS, and I can even see the hex color and this specific value right here. So why do I have that and it's actually super useful. If you're using Visual Studio Code, go ahead inside of your extensions here and search for Tailwind. Inside of here you will find Tailwind CSS IntelliSense. If you're using Tailwind, please install this. It will make your life that much easier. So besides that, it also offers you autocomplete. For example, if I want to add font bold or extra bold I can just do this and there we go once I save it turns bold so besides that it will also kind of hint you if you're using an invalid tailwind class if I accidentally add a typo here when I hover over nothing happens but whenever I hover over a valid tailwind class I can see the underlying CSS so using that little trick you can always check if you made a typo or not for what you think is the tailwind class. Perfect. So our tailwind is actually working. The reason our tailwind is working, I'm not sure if you even like noticed that it's working. But I meant to say is previously, tailwind required a config file, which we cannot find at the moment. That's because we are using tailwind version four, as you can see. So tailwind version 4 if you're not familiar is well an upgraded version of Tailwind which simplified a ton of things one of them being no more config file which is absolutely amazing. You don't really have to worry about this I'm just making sure that you are aware what Tailwind version you're using but if you install the app by using 15.5.4 you will have the exact version as I do. So yes another thing to check is the package JSON just double check that you have the same dependencies as me so you don't get any frustration and having trouble follow along. The biome part or sLint part isn't really important. That's really for your own linting preferences. Perfect. So now that we have that, let me go back and see what I've outlined here. So, so far, we have set up the environment. We set up Next.js app, but we didn't set up ShadCN UI. So what is ShadCN UI? I like to say that ShadCN UI is a component library, but that's actually not true. ShadCN is much more than that. So as per their definition, ShadCN is a set of beautifully designed components that you can customize, extend, and most importantly, build on. Start here and then make it your own. That's exactly what ShadCN is. Let's go ahead and click get started and let's select next.js. In here, I'm going to go ahead and select npm because that's the package manager that I am using. So before we run this again, instead of doing that, I'm just going to go ahead and do something else. So I'm going to go ahead and select latest dash dash version. This way, you will see the exact latest version at the time of me making this tutorial. so instead of running latest init I will run 3.3.1 and I would highly recommend that you do the same so you don't have any surprises if some components were deprecated or drastically changed even though ShadCN really really takes care of backwards compatibility and there's a little bug in my command here it is not chat cn ui it is just chat cn make sure you don't do the same mistake and make sure you're doing this inside of node-based directory so before i run this command ls make sure you see these things and once you've convinced yourself that you're inside of here go ahead and run npx chat cn 3.3.1 in it let's go ahead and allow it to install the packages and once it's done that it will add a couple of changes a couple of new files folders inside of our project so when it comes to which color you would like to use it really doesn't matter but if you want your project to look exactly the same as mine select neutral and the rest will be found automatically you can see it recognized the next js it found version 4 tailwind config it found the import alias basically no problems at all setting up shad cnui perfect so what i would recommend you do is just restart your server here just in case because this was a lot of hot reload because of all the files that have been added here so you can see now instead of my source control here i have a lot of new files so i have components.json i have a bunch of new files in my package json I have changes in my globalist.css. I have changes in my page. Okay, that's us. We did this. And I have a new utils file. So let's go ahead and just quickly go over these. And I will try to explain what they are. So starting from the obvious new folder lib, and inside we have utils.cs. So this will probably be the most used function in this project. And I can immediately show you how it's used. So head inside of app page.dsx. And instead of writing class names like this, imagine you had something dynamic. For example, const something and set it to true. And go ahead and open curly brackets around here. And then add cn from lib utils. So that exact function that I just showed you, right? And this is our import alias. So this is so you don't have to do this. It doesn't make too much sense now because we are very shallow, but imagine you are in a deeply nested folder. You would keep having to doing this and this and this until eventually you found libutils, or you can just use the import alias, which kind of teleports you to the root of the project. So make sure you added this and make sure you have wrapped your class name like this. And looking at it, nothing really changes. Let me just refresh here. So nothing really changes. And yes, if you were in dark mode and had a black background, after adding ShadCN UI, you will now have a white background. So yes, that's perfectly fine. So nothing really changed regarding the class name for our Hello World text, right? Even though I just wrapped it in this CN library. Well, yes, because by default, it can do just that. Just normally parse your Tailwind class. but imagine you had a bunch of dynamic options for this class for example if something is true in that case change the text to green 500 and you can see that now it works so technically yes you could have done this using template literals but trust me we're gonna have a bunch of these things There will be if something is equal to, I don't know, maybe, right? And basically a bunch of these options, each of them having their own class name, and it just gets so much easier to work with CN instead of template literals. So I just wanted to quickly explain to you what the CN is. Perfect. So right now, that's the only thing that ShadCN added. but what it actually added is kind of its config and its registry so what i like to do now is i like to add all chat cn components so let's shut down the app for now and let's go ahead and do npx chat cn 3.3.1 add dash dash all so instead of adding individual components i'm going to add all of them chances are we are not going to use them all and you can definitely clean this up The reason I like to add all of them is A you can explore and play around with all of them and B I kind of forget the Shatzian version during my tutorials, so it's just kind of a hassle having to remember the version every time I want to add a component and have to worry if I entered an incorrect version. So this way, in the beginning of the tutorial, we just added all components, here they are and we will be able to use them so go ahead and do npm run dev and now inside of your source file you will see a new components and then ui folder and inside of that ui folder you will see all the components that we will be able to use and you will also find a hooks folder which has the use mobile hook and this hook is simply used i think maybe in the sidebar let's see was I correct it is use is mobile there we go so yeah it basically created some hooks as well so you should have 60 new changes if you are using any source control to track that so let's go ahead and just add a simple one to see this work so I'm going to go ahead and very simply import button from components UI button and I will render a button here and let's go ahead and say click me let me go ahead and give this a class name height full minimum height of screen minimum width screen flex item center and justify center let's refresh this every time you restart your node.js server make sure you hit refresh there we go so we have a nice button here so what's the catch with chat cn how is this different from material UI or anything like that. Well, here's the thing that I personally like the most. If I want to modify this button, I could just add a class name and modify it the way I like. But I can also do this. I can literally go inside of source, components, button, and I can see the entire code inside. So for example, inside of my root page here, I have a variant prop, and you can see that it auto-completes the type safety, for example, outline. What if I don't want to call it outline? What if I want to call this border? Usually, you'd have no way of changing that within a material UI or something like that. But in here, you have the code. You own the code. So if I want to go inside of the button, find the button variance, find outline, and change this to border, you can see that immediately type safety is broken because now it will recommend border. And there we go. Works again. So I will bring this back to outline now simply because that is the correct and the better name. I was just demonstrating, right? But the cool thing is you can add anything here. You can add new sizes. You can modify existing sizes. You can change the roundness. You can change the shadows. Whatever you like or don't like. It's almost like a perfect foundation. Not almost. It is the perfect foundation for building your own component system. And that's what we will be doing in this tutorial. We will be making our own component library with the design that matches our entire app. And the easiest way to do that is with ChatCN UI. And it is extremely well supported within the Next.js environment. Amazing. So I think this kind of is a very good start. I would recommend that you maybe import some more components here. perhaps go to chat cn ui here ahead inside of the components and just you know play around there's a bunch of them here for example the calendar well i mean it makes no sense for me to go individually into all of them but yes there is a very good amount of these components and when you see this blue buttons it means they are new components for example field is completely new or let's go ahead and look at empty. We now have empty states that we can do using ShadCNUI, which is exactly the field that we actually will be using in this tutorial. All right, cool. So we just added ShadCNUI. We have that ready. And now let's go ahead and create a GitHub repository. So what I'm going to do is I'm going to go ahead and head to github.com and create a repository. So after you went to GitHub, you will see a little plus button here and click new repository. And then in here, let's go ahead and call this. I'm going to call it node base like that. I will set this to private and I won't add anything else. Right. And let's just go ahead and click create repository. And now in here, you have two options. The first option is create a new repository from the command line or push an existing repository from the command line. Our case is the second one. We already have a repository. It's just local. So let's go ahead and do the following. You can see we have 60 uncommitted changes here. And I want you to be careful now. So if you're using Visual Studio Code, you can use this interface to commit. But in case you aren't, you can use git commit commands, right? I think it's a little bit easier to follow the tutorial when I do it visually. So go ahead and click inside of the source control here and find the plus button, which says stage all changes. Be very careful not to accidentally click the discard all changes, right? Make sure you click stage all changes. And once you've staged all changes, what I like to do here is I like to name my messages after my video chapters. Obviously, this is not how you would do it in production. in production, you would actually describe what you did. But in my case, I call this chapter 01 setup. So my commit message is a 01 setup. And I will then click commit. Now the problem is this is only committed locally. So this doesn't exist in this GitHub repository. But only once you've done that, once you have no committed changes, can you go ahead and copy these three lines from the new repository you've just created. Let's shut down our app here. You can see how I have zero uncommitted changes and I will very simply run those three commands right here. So let's go ahead and add them. There we go. Go ahead back here and refresh and here we have our project 01 setup and we have another commit from the initial create next app. Perfect. So inside of our source folder you can see we have components and everything we did during the setup chapter which is add all of those components amazing great i'm very satisfied with this i believe that marks the end of this chapter feel free to play around with the components in the next chapter in the next chapter we're going to start exploring the layout and some more things about this project amazing amazing job and see you in the next chapter In this chapter, we're going to focus on setting up our database and our ORM. We're going to start by installing Prisma into our project. We're then going to connect Prisma to a Postgres database using Neon. Then we're going to explore Prisma Studio. And finally, we're going to explore and test Prisma API. So where we left off last time was this click me button. What you can do now is you can actually shut down your app. So as you can see, I don't have any server running. And then once I refresh, I get this screen. So that's perfectly fine because we're just going to focus on installing packages for now. So using the link on the screen, you can go ahead and visit this guide on how to use Prisma ORM with Next.js. So the same as in the beginning, as you can see, prerequisites are Node.js 18+. So again, I highly recommend that you are on a more up-to-date version of Node.js. So the step one here is to create the project. We can skip this because we already have a Next.js project. So no need to do this at all. Instead, let's go ahead and let's actually just focus on installing and configuring Prisma. Let me zoom in a little bit for you here. And in here, it's important that you change from Prisma Postgres into other databases. So it's a subtle difference. We are not going to need this. So switch to other databases here and let's do a dev dependency install of Prisma and TSX. So inside of my node-based project here, I'm going to go ahead and install Prisma, TSX, both of them as dev dependencies. Once they've been added, I'm going to install Prisma Client in my normal dependencies. and Prisma and Prisma client are pretty important packages for this project so I would definitely suggest that you go ahead and immediately go inside of your project and just visit the package JSON. The reason I'm telling you this is so that you can see how far ahead your versions are so I am on the version 6.16. You don't have to be on the exact version as me Prisma can update quite frequently. Same thing for TSX but I would recommend at least trying to be on the same major version as me. I highly doubt that there will be any breaking changes that soon in the next major version. Most of them are pretty backwards compatible but then again if you just want to install the exact same version as me here is the version that I am using but I am very certain that you will be able to follow along with your version as well so no need to do anything special about that. Now that we have installed these packages let's actually initialize Prisma. So in here they give you the full command but we actually won't be needing these two at all. So let's just run npxprisma init. So I'm going to clear this npxprisma init again inside of this project and let's just wait a few seconds and there we go. So after this has been completed you can see that the Prisma schema was created at prisma forward slash schema dot prisma. So now when you go ahead inside of the Prisma folder you can find schema dot prisma. Inside of here you can see the provider, you can see the output and you can see the default database database provider is set to Postgres and the URL is reading from our environment file and it reading the database URL This is something we don yet have So let me again confirm Yes so only that file has been added schema And as you can see gitignore has been modified to ignore source generated Prisma, because this is where the types for our database will be generated later on. So right now, what's the problem is, instead of dot environment here, you should have it, because this Prisma init creates it, right? And you will have this very large comment inside. Now we're going to slightly modify this. So I'm going to remove this entire comment here and I'm going to set this to be just a simple comment database. And then I'm just going to go ahead and make this an empty string because now let's actually set up a scalable production ready database here. so we can close this now and we can head to neon using the link on the screen here so just go ahead and create an account and once you're inside let's go ahead and let's create a project so i'm going to call this node base these really don't matter you can just hit create and once you have a new project simply click connect to your database and in here you can just go ahead and click copy snippet so this entire snippet basically and once you have that that's going to be your database url as simple as that all right so now let's go inside of prisma schema.prisma and let me just tell you a little tip before we start writing anything here you've probably noticed that i have this clear syntax right i can very clearly see what's a value, that's a variable, and this specific syntax here. If you want the same, I highly recommend that you install the Prisma extension for Visual Studio Code. So that's how I get the syntax highlighting here. Great. So that's pretty much it for the Neon database, actually. It's super quick, it's scalable, and I love how good the developer experience is here. and you can also have a bunch of branches. It's basically perfect for use cases like this. Let's go ahead and let's see what we actually have to do next, right? So we just set up Postgres database and we set up Prisma ORM. But in order to explore Prisma Studio and to test the Prisma API, we actually need to create some models, some tables for our database, right? And we actually do have this Prisma guide teaching us how to do that here. So again, you can use the link on the screen if you want to go here. So let's go ahead and just do that. Let's add model user here. And let's add model post. So I'm going to just zoom out a bit. If you are not copying and pasting, it's just these two simple models. So we have a user model, which has a relation to many posts, and we have a post model, which has a relation to single or one user. Now let's go ahead and let's push this to our database. So the way we're going to do that is we're running npxprisma-migrate-dev. and if you want to, yes, you can add the flag dash dash name, but I'd rather we do it without just so you can see how it usually prompts you for the name. So make sure you have added that in your schema Prisma. I mean, you don't really have to add this specifically. You can add whatever you want really. I just copied their example. So I'm now going to run npx Prisma migrate dev is going to read my schema file and it's going to ask me after it connects of course to neon db is going to ask me what do i want to name this migration so i'm going to follow their example and i'm just going to call this init and there we go we now have a new migration file here since this is development these migration files aren't really too important right in production obviously this would be the way you would amend your schema but in development you can pretty much always just remove the migrations folder if you want to start over if something has gotten complicated if you feel like you've messed something up you can always do that so now that that works, what you can actually do is you can run Prisma Studio. And Prisma Studio will open on localhost 5555. And you can see that now I have post and I have user models. And I'm pretty sure I can actually just add a record here. So for example, Antonio, mail.com. My name is Antonio. I will leave the posts empty. And I don't have to add this. You can see a function will do this for me so i just click save change and this should automatically add a new record here there we go and in order to confirm that this actually happened on the server in our database head back to neon and inside of here they also have their own tables and there we go post nothing user we have one user with id of one email antonio and name antonio exactly what we wanted so now let's go ahead and let's actually try and query those things so what actually happened when we run npx prisma migrated that well besides creating a new migration and applying it we also generated prisma client what that did is let me just try and find it generated folder here it is so this folder right here now holds the types for our schema So we can now safely access user.email and it will tell us that it's a required string. Or for example, user.name, it will tell us that that's an optional string. So that's what the generated folder is for. And that's why it was added to gitignore here. So let's go ahead first and let's establish the Prisma client. So here's what we have to do. And I can't really remember where I found this exact guide, but I will explain you why we need to do this. So let's go inside of the lib folder and let's create a new file called database.ts. Let's go ahead and import Prisma client from generated Prisma. let's define const global for Prisma to be global as unknown as open an object Prisma with a type of Prisma client. So what this is doing is it's simply adding to the global object a new Prisma property. And I'll explain why we are doing that in a second. Let's define the variable const Prisma to be global for Prisma.Prisma or a new Prisma client. Now let's check if process.environment.nodeEnvironment is not production. In that case, global for Prisma.Prisma is equal to the Prisma we just initialized. And let's export default Prisma. So what's going on here? Well, technically, we could be doing this. We could just import Prisma Client every time we need it. The problem is we would also have to do new Prisma Client every time. So, okay, why didn't I just do this and we don't need this and we don't need this? And this way, I would simply use this singleton instance. Well, that's because that wouldn't happen because of hot reload. What's hot reload? Whenever your app is running, so if I do npm run dev, what's watching the changes in my file here? You can see when I, for example, add, if I add a little letter here and click save, actually, this is a bad example here. Let me just refresh localhost 3000 first. basically hot reload is watching for changes right so if i go inside of my app page.dsx click me too you can see that it immediately updates right so it immediately compiled so that's exactly what's happening and the problem is when hot reload happens it will create new instances of prisma client multiple times and you will start to get warnings in your terminal about that. It will basically tell you multiple Prisma instances detected. As far as I'm aware, it's a development-only issue, but it can severely degrade the development speed when it comes to the dev server. It will become quite slow and maybe even some bugs will occur. So how do we fix that? Well, we're using a little hack here. And I mean, when I say hack, this is an official solution, right? I'm just really not sure which part of the documentation talks about this. But basically, one part of this environment which is unaffected by hot reload is global. So global is, well, a global variable, right? What we're doing here is we are storing the new Prisma client inside of that global.prisma property. what we did here is we simply created a special type that can understand what the prisma is if we didn't do this it will still work you would just get the type errors i mean technically you would do it like this then right global dot prisma so yes technically this works except it has no idea what prisma is so we just created a type for it right here because you can't put anything in global. So doing this ensures that no new Prisma client instances are created during hot reload. That's why we only do this in development mode. There is no need to do this in production, right? In production, essentially what happens is this. This is what will happen in production, what you would expect to happen, right? But in development, we need to complicate it a little bit by adding it to the global object because global is unaffected by hot reload, so no new instances of Prisma Client are created. Okay, I hope I managed to explain that. Great, now that we have that, let's go inside of source and...\napp page.tsx. So in Next.js, every page file by default is considered a server component. I can go in depth explaining server components, but I think it's best to show you an example. In server components, things like use effect do not work. So if I just go ahead and add a simple useEffect here and save, I will get an error. In order to fix it, I need to convert it to a client component by adding the useClient directive, and that fixes it. But the cool thing about server components, while they can't accept useEffect, is that they can be asynchronous. And once they are asynchronous they can do cool things like for example my users can be queried using a wait database my apologies prisma from lib database dot user dot find many and then inside of here let me go ahead and do json stringify users and once i save this there we go i successfully fetched the user from my database directly using prisma in a server component. So that's the power of server components. It technically acts like an API route. Obviously, this isn't too safe. So don't worry, we will add a proper data access layer using trpc. And we are going to leverage both client components and server components. Because one thing that server components are good are good at is that they render. Well, I'm not sure if render is the correct terminology, but they appear before client components. So it would be wasteful not to use them to already start fetching something because it definitely will make the app faster. But we will talk about that in future chapters. For now, I just wanted us to achieve this very simple thing. I wanted us to add an item to our database and I wanted us to fetch it. So what if you want to remove all items from your database? Well, you can do that manually using Prisma Studio, but you can also do this, which I think will be quite useful for you. You can run npx Prisma Migrate Reset. Obviously, only do this in development. Don't even think about running something like this in production. It will ask you to confirm. All data will be lost. Confirm. And once you do that, well, all data is deleted. So there we go. immediately if you refresh you will see it's empty perfect so for now I'm going to leave the Prisma schema as it is with this user model and the post model turns out we didn't need the post model at all sorry for making you write it but we will write our own models pretty soon and then I will explain in depth about this relations in this foreign keys how does all of this work how does the syntax work. I purposely didn't want to get into it right now. We'll just use it as a quick example to try and fetch something from our database. So in the next chapter, we're going to focus, as I said, on the data access layer so that we can actually learn how we are going to fetch. Because while this is cool, it's not exactly the safest and it's not really scalable. So let's go ahead and see. Let's remove the unused button import while we are here. And let's see inside of here, what did we achieve? We definitely explored Prisma Studio and we tested the Prisma API. So what I want to do now is I want to push to GitHub, but I want to do that by creating a new branch and creating a new pull request. So we learn the actual Git workflow. And then let's review our pull request. So all the changes we did and let's merge it. So I'm going to go ahead like this. instead of your source control you should have eight files so git ignore package lock package json schema two migration files page and the database lib so what i'm going to do now go ahead and click on the main here create new branch and i'm going to call this a zero two database and you can see that now I'm on my new branch. Now I'm going to stage all changes. I'm going to name the commit the exact same. So 02 database and I'm going to click commit and then I'm going to click publish branch. If for whatever reason you already clicked commit and you think, oh, how do I go back? It's okay. So Git workflows are optional to follow in this tutorial. I just think it's a good idea to give you an idea of how it would usually be done in branches. You obviously don't have to do it if you don't want to. So if you just already committed, perfectly fine. No problem. So now that we have pushed this branch, let's go ahead and open a pull request if you want to follow the Git workflow and let's review it. So if you go to your NodeBase repository, you will now see 02 database had recent pushes. But just in case you don't see this, you can always go manually inside of pull requests, click on new pull request, leave the base to be main and compare to be your new branch database. Go ahead and click create a pull request and again, create a pull request. And now we're gonna go over our files and review them. And now you've probably noticed that I have some kind of summary. here, which you probably don't. And that's completely okay. That's expected. So let's first go over what I have here. And then I'll show you exactly how you can have the exact same summary for every single one of your pull requests. So summary by CodeRabbit. New features. Homepage now dynamically loads and displays user data. Correct. Chores. We added database tooling and supporting dependencies, Prisma. We initialized the database schema with users and posts, including relationships as well as constraints. We added a migration lock configuration. And we converted the homepage component to an asynchronous component to support server-side data fetching. That's exactly what we did in this chapter. And it's exactly what CodeRabbit summarized right here. So in here, we can see a more in-depth walkthrough file by file. So for example exactly what we did here and the summary but what I really really love are these sequence diagrams. Right now we have a super simple situation in our app so it's not too impressive but nevertheless you can imagine how useful this will be later on especially when we introduce this complex long-running background jobs and all of the different states that can happen here. So let's go through this sequence diagram generated by CodeRabbit. So once the user visits the root page, what we do is we use the Prisma client. So we import the singleton file using from source lib database.ds. Using the Prisma client instance, we fire Prisma.user find many, and we contact the Postgres database. The Postgres database then returns an array of users, and we finally render that in a form of JSON users on app page.dsx. So I absolutely love how CodeRabbit does all of this for us here. Obviously, this is a super simple pull request, so there's no need to review much now. But later on, we are bound to introduce some bugs in our projects, even some critical mistakes. And you will be super impressed by how CodeRabbit will catch this, and it will alert us before we merge that into our main branch. in my previous few projects this has saved me countless times so if you want the same thing you can go ahead and use the link on the screen and simply create an account with code rabbit you can get it completely for free and if you don't like it in pull requests you can also install their extension so just go ahead and install the code rabbit extension here and connect the account which you've just created and you will get completely free AI code reviews for your branches and your pull requests inside of Visual Studio Code. Of course all of this is completely optional I just think it's extremely useful but to each their own. Perfect so I've just reviewed this pull request it's fairly simple I reviewed the sequence diagram it's exactly what we were expecting to happen. So let's go ahead and merge this pull request and let's confirm the merge. So I'm not going to delete my branch for a very simple reason, history. So inside of my code base here, I like that I have a 02 database and I can always go back to it if I want to see the exact changes I did. So I've just merged a 02 database inside of my main branch, which means that inside of my Visual Studio Code, I also have to go back inside of my main branch. So I'm going to hit on 02 here, and I'm going to select a main. You will have two main branches, this one, and origin main. In our case, it won't really matter which one you choose. So one of these is remote, and one of these is local. Since we're not doing anything complex, most of the time, they will be exactly the same. But regardless, whichever one of these you choose, both are main, you can see main here, main here, whichever one you choose, always make sure to click this little synchronize changes button, and then click OK. And this will push and pull all the changes from the GitHub repository. So you can see that now my main branch is completely in sync, and it has the Prisma folder, even though I developed that in a new branch. And in order to confirm you did this correctly, in your Visual Studio code, you can go inside of source control and click on the graph here. And you can see how I developed a 01 setup on one level. And then I detached into a new branch here for my 02 database. And then I merged that back inside of my main branch here. So this is how it should look like if you followed my git workflow. Again, you can complete this entire tutorial without following the git workflow I just think it useful to know but to each their own you know why you here you know what you want to learn from this tutorial Excellent. Amazing job. And now let's go ahead and just confirm we've done everything we wanted for this chapter. Looks like the last thing here was to push to GitHub. And that's exactly what we did. We created a new branch, we created a new pull request, and we reviewed and merged our changes. Amazing, amazing job. And see you in the next chapter where we're going to develop our data access layer using the RPC. In this chapter, we're going to focus on setting up the RPC, 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.dsx, 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 API 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 these 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 am 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 five, my trpc is version 11. And my trp, 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 7 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 backend 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, instead 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.ts and inside paste the code from here again let's review 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 create trpc router 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, dot query would be a get API call. If I did dot mutation, this would be a post API call. Well, or put or patch, right? You get the gist. We're just using an RPC way of building this API routes. We still haven't touched the create caller factory because it's a little bit complicated to explain. But for now, I hope you understand. Okay, so the RPC 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 going to be very clear what create the RPC 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 going to 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 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 trpc 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 uh 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 add sign. So just change this to the add sign. And this too. And that's it. In here, it's important that the endpoint is correct, forward slash API, forward slash drpc, which is exactly what we have established here. so there isn 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 the RPC 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 the RPC query-client.ts. Let's go ahead and let's do that. So inside of source, the RPC, 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 tan stack react query, default should hydrate query, dehydrate query, and the query client. What you're seeing here is basically just a tan stack query configuration that works well with the RPC. I have never ever modified this. It just works fine as it is. It's basically a single ton tan 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 super json 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 use client 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 us that we have created. And then we create the provider. In here, as you can see, it's very clear what they're doing, right? They are initializing get query client in a way, well, 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 trpc provider and query client provider so think of it as a well 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 yes 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 the RPC and I'm going to create server dot not dsx actually yes it is dsx but 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 right that's why this is very important and now this is actually not needed so if your router is in 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 use query and use mutation. 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 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 the RPC. 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 LibDatabase.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 Zod import from here. Look at 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, 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.create caller 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 the RPC 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, the RPC, and well, 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. Whoops. 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. All right, so now that we have this, we have solved this super simple example of fetching this in a server component, right? we repeated the example that we had but what if this was a client component so what if I had use 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 danstack react query and now we do trpc dot get users dot query options 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 the RPC 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 uh do that let me just try and follow the proper example okay so they're doing it with this hydration boundary uh 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 like 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 get query client 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 tanStackReactQuery 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 dot get users dot query options so what this is doing is it is leveraging the speed of a server component by instantly starting to prefetch 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 use query But since we are prefetching here, we can almost expect it by using use suspense query. Let's get DRPC, use DRPC from the client and pass in DRPC, get users query options. And then let's go ahead and try it out. Now, 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 queries 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're going to leverage the power of speed components but we are going to hand off to a client component which is familiar for us to use right i am sure that all of us are more familiar with hooks rather than you know 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 use suspense query here, I can get the errors, I can get the loading state, I can refresh, 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 TRPC. We created a procedure with Prisma API. We explored TRPC 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, 0.3 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 commit03 to your PC setup, commit, and let's publish the branch.\nand 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 code rabbit 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 a 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, 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 copy 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 infer the context type. We are going to do that but later. We don't need it now. And 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 site 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 synchronous 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 pr 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. In this chapter, we're going to focus on adding authentication to our project. We're going to do that by implementing better alf, a new alt library that is slowly becoming the standard for Elf everywhere. So let's go ahead and start by following the Better Elf documentation. You can use the link on the screen to visit their page. Let's go ahead and click get started right here and let's go ahead and click on installation. I'm going to select my package manager which is npm and I'm simply going to run npm install Better Elf. Let me go ahead and just make sure that I'm not running my app at the moment and I don't need Prisma Studio either. So I have no servers running just npm install better out. And immediately I'm going to show you what version of better out I'm using since it is a pretty important package here. So as you can see I'm using better out 1.3.26. So as always my advice is the same as for trpc. If you are on the same major version as me you're probably not going to have any problems. But if you're watching this from version 5 of Better Out you will most likely have some problems and you should probably use the same version as me and then upgrade later. But given what I've experienced with Better Out so far is that they take extreme care when it comes to good developer experience. So I have no doubt that they will take care of all kinds of backwards compatibility. But still, I'm showing you my version just so you're aware on what version I am building this authentication module. Now let's go ahead and let's set up better alt secret. And they've generated this handy button to generate secrets for us. So let's go ahead and go inside of dot environment here. Let's add better alt. and let's add better alt secret. I have a preference of adding these in quotes like that. Let's go ahead and see what else we have to do. We have to set the base URL and we can actually copy this as it is because it is HTTP localhost 3000. If you're not sure what is your app URL, go ahead and run npm run dev and in here you will see local HTTP localhost 3000. Great. Now that we have that, let's go ahead and create a file named alph.ts inside of the lib folder. So I'm going to go inside of source, lib, and I'm going to create alph.ts. In here I'm going to import better alph from better alph and then export const alph better out and let's leave it empty as in the example above in here we are told to configure a database here so let's go ahead and look just below where it says alternatively if you prefer to use an ORM which is our case we use prisma let's click on the prisma tab and let's see how to configure it so first things first we have to import a prisma adapter so import prisma adapter from better out adapters prisma then let's go ahead and import prisma or is it database my apologies i'm too zoomed in so i can't see where my imports are coming from i think it's prisma from dot forward slash db or if you prefer you can use it like this so you know exactly where it's coming from so once you have that let's go ahead and let's do database prisma adapter pass in prisma open an object provider postgres because that's the database that we are using for prisma so we import better out from better out package we import prisma adapter from better out adapters Prisma and Prisma from lib database, which is this little util we've created in the previous chapters. What we have to do now is we have to create database tables. What I would suggest you do right now is you go ahead, let me just close this, you go inside of source, my apologies, inside of Prisma, schema.prisma right here. And you can go ahead and copy this entire file, just in case. You actually don have to worry because if you did Git workflow the same as me you can easily revert your changes But let go ahead and let run npx better forward slash CLI generate So I'm going to go ahead and run this inside of the root of my application. It's going to recognize Prisma. So let me just confirm this update here. And now you can see exact CLI version that I'm using in case you want to use the same version. so it noticed prisma schema and it tells me that it will overwrite it because it will write directly into that file let's go ahead and confirm that because we don't really have anything important there but that's why i told you to copy the file just in case you had some important things i don't know how exactly you're following this tutorial but nevertheless in my experience it never actually overrides the entire thing yeah as you can see i still have uh something here and i think we actually might have a problem here it's because we already had the user object yeah let's go ahead and do this let's remove the entire thing let's just leave it empty schema prisma like this and let's try again yes and now you can see the user model is correct the reason we had a problem is because we had an existing schema Prisma and that schema Prisma already had the model user. Because if you remember, when we followed Prisma guide, we copied this model right here of the user. So it ended up having a problem, right? That's why I just deleted all models now and then I rerun the generate command. It found the Prisma schema again and then it populated it correctly. So let's go ahead and see what fields we are supposed to have inside so you can see whether you have the correct ones too. We should have the user with an ID, name, email, email verified, image, timestamps, session, accounts, unique field for the email and map to user. then you have the session, which has all of these fields, and the relation to the user, as well as these unique and map fields. Account and verification. These are the fields you're supposed to have in your Prisma schema, and you should have no errors. Save that file now, and let's go ahead and migrate it. So npxprisma.migrate.dev, like that. and let's go ahead and call this better out once we get the prompt keep in mind that if you have existing data inside you will most likely have to delete that data you can do that by using npx prisma migrate reset this way you can you will delete all of your data for example you can see the warning i'm getting here you are about to drop the user's table so yeah in my case that's completely fine. Better out fields. There we go. So now we have a new Prisma schema. And if you run npx Prisma Studio, you should now no longer have any users in here. But you should now have an account model, session model, user model, and verification model. All of these fields are needed to properly run better out. Perfect. So once we have done that migration, I think that now we can do npm run dev. And let's see what we actually have to do now. So we didn't run the migrate here because that's only for specific adapters, not for Prisma. But the Prisma migration script is very easy anyway. So that's actually it when it comes to what we have to do. So for example, let's now enable email and password login. We do that so simply by going inside of source lib alf.ts, email and password enabled, true. That's it. That's how you enable email and password login in BetterAlf. And now let's go ahead and let's build a form that will accept those fields. So I'm going to go ahead and I'm going to go inside of source app. And let me create an alf route group. and let me create login and inside of here page.tsx const page return login export default page so what I've done is I've created a reserved file name page with a default export and I've put it inside of a login folder inside of the app folder and in between those two i've created an out folder in parentheses what this is used for is to organize your folders your routes your groups right without affecting the url so the way you can access the login page now is by going to localhost 3000 and then forward slash login. And then you can see the text login here. So notice how, let me just show you here, HTTP localhost 3000 slash login. So that's my current URL. So notice how I didn't have to write out inside. That's what this is used for because usually inside of the app folder, any encapsulating folder name, which holds a reserved file name page becomes part of the url well if you want to avoid that and you just want to use folders for organization purposes like i am now for out you can add parenthesis and then it will not be a part of the url you will just simply go to forward slash login perfect so now that we are inside of the login here let's go ahead and build a form so the place where i'm going to do that is going to be inside of source i'm going to create a new folder called features and inside of here i'm going to create an alf feature and then in here i'm going to create components let's go ahead and create a login form.tsx let's mark it as a use client and let's go ahead and add all the imports that we are going to need. So the first imports that we're going to add are going to be Zod resolver from hook form resolvers Zod, image from next image, link from next link, use router from next navigation, use form from react hook form, toast from sonor, z from Zod and button from components UI button. If you're wondering where do all of these imports come from they all come from the chat cn ui command that we run in the first chapter so if you remember we run npx chat cn add dash dash all what that did amongst adding all components was also add a bunch of package json files my apologies a bunch of packages in the package json to make all of those components work so yes you already have for example react day picker you already have where is my react hook form here it is react hook form you already have zod you already have hook form resolvers so that's where all of these imports are coming from all right now let's go ahead and import everything we need design wise so besides the button we're also going to need every component from components ui card and then we're going to need everything from the form field. So form, form control, form field, form item, form label, and form message. And last three imports are going to be an input. And we're going to need out client. Let's comment this out for now, because we are getting an error. We will come back to this later. I think I forgot to do that. Yes, it's a super simple thing. But we can continue with what we're doing right now. So we don't lose focus. So let's start by defining the form schema. So this form schema, which is login form, is going to have two fields, email and password. So let's define that using zod. So login schema is equal to zod.object, which accepts email, which is a type of z.email and password, which is a type of z.string with a minimum value of one, as well as some error messages if that rules are broken. Now while we are here let's quickly define the type for this form. So login form values are using z as zod.info type of login schema and let's export function login form here. Now let me just fix this. The props are going to be actually we don't need any props here. We can just go ahead and do this. Now let's define the router to be useRouter and let's define the form to be useForm. So const form useForm. Now inside of this form, let's go ahead and give it all the properties needed to infer the login schema. So use form, open an object, add the resolver, zod resolver, which uses the login schema object, and default values, which are email and password. And in order to make this type safe, let's also properly infer login form values here. There we go. Now let's go ahead and define the onSubmit method, which is going to be an asynchronous login form values function. and let's just console log values let me fix the typo here now let's define is pending to be form form state and then is submitting this way we store this in a shorter named field so it's easier to look at our code really now let's go ahead inside of here and return a div. Let's give this div a class name of flex, flex column and gap of six. Before we do anything further, I want to render this login form component somewhere so that we can actually see what we're doing. Instead of this page, instead of this div, let's do a simple login form. From at features alt components login form. So right now, as you can see, we can't really see much. But we are going to slowly start to see that now. So inside of here I going to add my card element I going to go ahead and go inside of the card and give this a card header And I going to give the card header a class name of text center Inside, I'm going to add a card title with a text welcome back. Below the card title, I'm going to add card description with a text login to continue. outside of the card header i'm going to add card content and then i'm going to add a form element instead of this form element i'm going to spread the form constant which comes from the hook which we have defined above and now inside of this form let's go ahead and let's add a native form element like this. This native form element will have an on submit method of form handle submit and pass in the on submit. Then let's open a div inside of that form with a class name of grid gap six. Then let's open another div with a class name of flex, flex column and gap four. then let's add a button let's give this button a variant of outline let's give it a class name of full width let's give it a type of button so it doesn't accidentally trigger the submit method and let's disable it if we are pending and in here let's say continue with GitHub. Let's go ahead and copy and paste this. And let's change this to be continue with Google. There we go. Now, outside of this div, let's add a new div. And let's close it like this with a class name, which is going to be grid gap 6. add a form field which is a self-closing tag give it control property of form.control name which is email render let me just properly extract from the render field the field property and then inside of here let's go ahead and let's render the form item like this inside form label which will simply say email and then form control. Let's add a self-closing input tag give it a type of email give it a placeholder of m at example.com and let's go ahead and spread the field property like this. and now below the form control let's add form message here which is a self-closing tag in here we're going to render any errors now that we have established this we can go ahead and we can copy this form field so this self-closing tag and paste it below and this one will control the password field so change the form label to be password change the type to be password and for the placeholder, you can just add some asterisks here. Just like that. Now outside of this self-closing form field, add a button with a login text. Give it a type of submit, give it a class name of full with and give it a disabled if is pending. Just like that. And now, just outside of this div, add a new div with a class name text center and text small. Inside of here we're going to ask, don't have an account? Question mark. And then let's add a space like this. If you don't have an account, let's use the link to sign up by going to href forward slash sign up and give this a class name, underline, underline dash offset dash four. If you're getting an error for this asterisk, you can just use appos like this. So don't have an account. And when you click, it will lead you to a 404 page because we didn't develop it yet. but that's how we're going to switch between login and register so if you try it and hit login now you will just get some errors but even if you try uh submitting something the only thing you'll actually be able to see here are the logs because the only thing we added was console log values here so let me open inspect element here let me go inside of the console and once i click login there we go. Whoops. I can only see the email and the password that I have entered. If you see that, it means you've developed everything correctly. So before we go any further, I want to go ahead and copy this entire thing and create register form. So let's go ahead and copy the login form. let's paste it in this components folder and rename it register form and then go inside of the app folder go inside of the out go ahead and create a new folder called sign up or register whatever you prefer and then page.tsx inside I'm going to simply repeat a div which renders register form. Now we can't really import this right now because we didn't change the name. So we have to go back inside of our features out components register form and let's go ahead and immediately change the name to register form. So now we are exporting a function called register form which means that now we should be able to import that from features out components register dash form there we go so now when user clicks on sign up here they are no longer getting an error but they are also not seeing anything different from the login form so just make sure that right now you are on localhost 3000 slash sign up make sure you are on this page because we are developing the register form now or if you have named this register in that case go to register right just make sure you're looking at the new register form so in order to see that immediately let's change this from welcome back to get started there we go now i can see the text which says get started and instead of logging to continue it's going to be create your account to get started like this and then we can scroll all the way down to the footer which asks don't have an account and let's change this to instead ask already have an account and let's use the href to go to login and the text will be login and now you should be able to switch between the two login it says welcome back sign up it says get started. Perfect. Now let's focus exclusively on the login form. So I'm going to start by changing the name of the schema into register schema. And I think that we are using it in these three instances here. So I'm going to change all three at the same time using command D. You can change individually if you want to. But yeah, here's a little trick if you're using Visual Studio Code. Select the variable and then press command D two more times. So one, two, and that will select all three instances. And then I can change it to register schema. It's a cool little trick. Now let's go ahead and also add confirm password here, which is also going to be a string, but we're not going to add any rules to it directly here. Instead, we're going to add a refine method. Refine will get the data and it will check if data.password is equal to data.confirm password and if it's not in that case the message is going to be passwords don't match and the place where we are going to show this error is defined in a path confirm password so we are not going to show that in the password field we're going to show that in the confirm password field. And I think that you can even choose both of them if you want to. And then both fields will have the error. Great. Now I'm going to change the login form values. Again, three instances. So I use command D. If you are on Windows, perhaps it's control D. So one, two, like this. All three are selected. And now I can do register form values like this. And let's go ahead and add confirm password here to be an empty string. like this perfect we can leave everything as it is and we just have to add one more form filled so let's go ahead and copy it and let's paste it here and change the name here to be confirm password and change it here to be confirm password as well and let me see if that's the only thing we need we can actually do it like this confirm password type password yeah everything here should be the same and then change the sign login to sign up there we go and now if i for example type in antonio mail.com here and if i type 1 2 3 4 5 6 7 8 and 1 2 3 4 5 6 7 8 it works but if i change to 9 it doesn't work perfect and let me see there was this weird error let me try again I'm not sure how to reproduce that error but it had some weird error message maybe it was the specific hot reload state that I was in not sure but I think this should work normally all right just make sure you have added the default value for it excellent all right so now we have both a login and register fields here and now what we have to do is we actually have to create an account when we register because we can't even attempt to try to log in if we are not registered. So in order to do that we have to go back to our better alf library here and let me just scroll up because I obviously missed something here So I added out here but i didn add out client and here it is create client instance perfect so react that the one we are using yes we are using next js but we are using react for the front end right so go inside of lib the same place where we created alf.ds and now add alf-client.ds go ahead and import create alf client from better alf forward slash react and export const alf client here create alf client and execute the function so it looks like this right instead of here they say that you need to to add a base URL, except if you are on the same domain. So for us, it's actually enough for the snippet to look like this. That's all we really need. And now what we can do is the following. We can now go ahead, instead of source, features, out, register form, right here. And instead of just doing console log for our values, we can actually log in. So we can do await, out client, sign up. Let me first import out client from lib out client like this. Lib out client. And yes, you can now remove the commented out code because that's the exact snippet that we added. Dot sign up dot email like that. Open an object, add name to be values dot email. go ahead and add email to be values.email password to be values.password and callback URL for now can be a forward slash and it's URL like so. On success here my apologies, open another object. These are now the fetch options. So on success of this fetch let's do router.push to a forward slash on error let's go ahead and get the error message here and let's go ahead and do toast.error context.error.message the only problem is you can't call toast from sonar before you add toaster to your layout so let's quickly do that by going instead of source app folder layout and just add toaster from components ui sonor just like this now everything will work just fine and there's another thing you can actually modify here let me try and see if it's here or maybe it's if it's in concepts here authentication email and password here it is so we already did this right we enabled email and password but you can actually do even cooler things here let me see so they have documentation for signing out they have email verification here but what I'm trying to find is a very simple way using better alf that you can automatically sign in. So if I go instead of alf client, my apologies, alf.ts instead of source.lib, email and password, auto sign in set to true. What this will do is it will basically automatically sign in when someone registers because usually you register and then you have to log in again. So by adding this little Boolean, you will automatically log in. so now let's get inside of source app page dot t s x right here and let's clean this up so we use this as an example right so we no longer have to do that feel free to remove all of these imports and make this a client component and in here let's go ahead and let's just simply check our out state so let me go ahead and find that api here so this is server side and let's see how do we do it using client side here it is client so we already established the client instance we already tested this so here it is use session from create out client that's exactly what i want to do I want to see if we are logged in or if we are not logged in. So let's go ahead and add this and let's import create out client from better out react. Or perhaps I think we can just import. Let me go ahead and set up my out dash client. Yes, I think we should definitely use this out client here. Out client. Maybe not. Not 100% sure. Let me see dot use session. Here it is. And then we don't have to import this. There we go. So in here, let's see data and let's just do JSON dot stringify data like that. And let's add a button which we can import from components UI button logout and on click out client and let's go ahead and find the logout the sign out like this there we go but that's only if data is present so let's not render the logout button unless we are logged in there we go just make sure you have added use client to your app folder page.tsx and you can now remove client.tsx. We only used it as an example. Feel free to remove it. We no longer need it. Let's go ahead and try some things out now. So I'm going to go ahead and prepare some things. So first of all, make sure you have npm run dev running. And then let's also do npx Prisma Studio here. Right now, we should have absolutely no users whatsoever. Let me see what's the problem in here i'm not sure can i refresh okay looks like now it's fine and now i'm gonna go ahead here and first let's go to localhost 3000 just without any uh anything then you should just see no because we are not logged in and now if i go to forward slash sign up because that's where my register form is I'm going to add antoniomail.com 12345678 and 12345678 and I'm going to click sign up like this and looks like I've got an error so let's see what the error is about and if they actually know what the error is I think I forgot to add the API out, my apologies so let's go ahead back instead of getting started, installation I should have just followed this to the end, this is definitely my fault because i get very excited and i want to show you immediately uh so let me go ahead and find further usage for next js here because i think we also have to add this to our api list so let's see next js here it is yes so mount handler you have to select next js and then you have to add the out all route right here and then it will work so let's go inside of source app folder api let's go ahead and create out then inside of here go ahead and create a catch all route similar to the trpc one but in here we do the spread to catch all and then route.ts inside and then just go ahead and copy this and paste it i will show you exactly how it looks like so from our lib out file and from better out next js we map the post and the get routes it's actually very similar to what we do in trpc we met the get and the post routes as well perfect so now that you have this let's go back here and Let's sign up. Let's wait a second. And there we go. You can see I have my logged in session right here. And not only do I have this session here, but I should also now have my user here in the database. Let's refresh here. There we go. My name, my email, every information is here. My session, my accounts, everything is here. Perfect. And now I have this log out button. And when I click that, there we go. I am now logged out. Perfect. So now our authentication is officially working. So now you might be wondering, all right, how do I redirect? You know, so for example, let's say inside of our app folder page.dsx, let's now convert this back to a normal protected server component. so I only want logged in users to see this how do we do that so the way I like to do this is by going inside of source lib and creating out dash utils dot ds import headers from next headers import redirect from next navigation import alph from dot slash alph because we are in the lib folder. Let's define require alph function which is going to be an asynchronous method. Let's first get the session by doing await alph.api get session and let's pass in the headers using await headers. If there is no session, let's redirect the user to forward slash login. Otherwise, let's just return back the session. And then let's go ahead and copy this. Let's paste it. And let's call this require an alt. So the opposite. In this case, if the session exists, let's go ahead and return to, well, what will in the future be workflows, right? basically, actually we can now just do this. So if the user tries to visit\nlogin page while they are authenticated, we should redirect them back, right? So that's why I have these two handy utils. And the way I do it is very simple. So I directly do my out inside of the pages that I need to do. There are several reasons for that. The first reason is I can see exactly which page is protected, which isn't. So if I just do require out here like that, that's it. This page is now protected. You can see how I was immediately redirected. Now, of course, the first question is, can I use a middleware? And most of you are thinking of the Next.js middleware. You can use it, but only use it for better user experience. Do not use it as a security layer. There have been countless instances of alt libraries being broken into using the Next.js middleware. that is because next.js middleware should not be used as the out layer so there are many tutorials which teach you how to automatically protect many of your pages using the middleware and that is great you can do that there's nothing wrong with that right it's for better user experience but you that shouldn't be your last line of defense that's why we developed the data access layer. That's why we won't directly call Prisma calls within server components. Instead, we're going to do that through trpc. And we're going to develop something in trpc called protected procedure. And that way, we're going to have actual security layer here. So yes, if you want to, you can explore how to add this to the middleware. But I've given up on teaching people that because people think that that's a security layer. It is not. So when I say middleware, I mean very specifically on Next.js middleware, which is actually a different behavior than what you'd normally think a middleware is. For example, the RPC has its own middlewares, but using out in them is completely fine compared to the Next.js middleware, which is more of a proxy than a middleware. so yes what i just did in the page.dsx was basically enough to protect this so if i now go ahead into antoniomail.com let's first try one two three four five wrong password and click login oh i didn't i forgot instead of our login form we never actually developed any kind of submit method here so let's go ahead and do that so await out client from lib out client so you can now remove this comment and outline out client dot sign in dot email go ahead and add email to be values dot email password to be values dot password callback url to be a forward slash and then let's open fetch options here on success router.push forward slash on error get the context toast.error context.error.message like that and I think that then we don't need CN so we can remove that and I think we have the same case in register form image we will have image later but we won't have CN so you can remove CN from both and I think that now okay let's try logging in now we should get an error here invalid email or password but if I add the correct password one two three four five six seven eight and log in I will get redirected to a protected server component and if I try to manually go to forward slash login now I can still access that that's something we want to prevent from happening so let's go inside of source app folder out login page.dsx asynchronous method await require an out like this and now if i'm logged in i'm redirected to the protected server component and some of you might get the idea okay can i just do this in a layout file and this way i can protect multiple routes at once that is the same situation as the middleware you can use it if you want to improve user experience but you shouldn't use it as your security layer so basically what is a good security layer how do you know if you have a good security layer imagine all of your out for things like this breaks imagine this here breaks imagine page.tsx out breaks imagine your a next JS middleware route breaks? Will users, unauthenticated users, be able to get access to your data? If the answer is yes, you have a bad security layer. That's why our main security layer will be inside of trpc-protected procedure. And everything else that I'm doing right now is purely user experience. Because even if I, later on, if I remove this line, require out for my server component and I allow the user to fetch and prefetch whatever, they are just going to get a bunch of errors. So the only reason I'm doing require out here is so I redirect the user so they don't see those errors. So they are on the proper place, the login form. So I'm trying to explain to you that data access layer is the only security layer when it comes to this out that we are doing, that matters. Everything else, think of it as improved user experience, you know, where to redirect the user so they don't see the errors or broken pages, right? But if you have ALT inside of your data access layer, which is for us, TRPC, no data will ever leak to an non-authenticated user. And that way, you won't even care if you use the layout or if you use the middleware yes but still i like to be very explicit with my routes so i'm gonna go the same thing in the sign up here asynchronous await require an out so now if i go to forward slash register or forward slash sign up my apologies same thing i'm redirected to the protected server component amazing so now let's go ahead and wrap this chapter up by developing one last thing So let's see what I've prepared here. So we set up better alph. We added alph screens. We added alph utils. The only thing we didn't do was add a protected procedure. So let's do that now. So let's go ahead and go inside of source, trpc, init.ts. And in here, we're going to go down here to the base procedure. And let's export const protected procedure. protected procedure will simply extend the base procedure this is a good practice in case you extend the base procedure itself in the future so you don't have to keep track of you know if protected procedure will have everything that you've added to the base procedure you can just simply extend the base procedure this is chaining these are all middlewares but now i'm using again the word middleware these are proper middlewares the one that by definition are middlewares next JS's middleware is more of a proxy than a middleware. So in here, it's fine. So let's go ahead and do base procedure dot use asynchronous method. And go ahead and extract the context. And next. And go ahead and open this function. Let's start by returning next. And then let's go ahead and first see if we are logged in using await alph from lib alph so yes go ahead and import lib alph right here so alph.api.get session headers are going to be await headers from next headers so make sure you have imported headers from next headers in case there is no session we are immediately going to break this by throwing a new trpc error from trpc server so just make sure you have imported this let's go ahead and add code unauthorized and message unauthorized and let's go ahead and extend the object that next will send by adding context extending the context and adding the out object as the session which we just fetched right here this way whatever procedure uses protected procedure and if the error is not thrown we'll have access to the current user id to whatever we care about for this session so we can properly query only the documents and records for this logged in session that is the protected procedure. And I didn't really explain, but I use this three times now. So this thing that I'm doing, get session, which we also have in out utils, get session, it's all documented in the better out docs. Let me try and find it. I think I have the concepts API. Here it is. Get session await headers. So that's how you do it on the server side. You need to await the headers. All right. So now what? Now that we have this protected procedure, let's put it to use. I'm going to go ahead inside of source, trpc, routers, underscore app, and I'm going to change the base procedure here to be protected procedure. And now in here, I should have context. and now in here I can actually do a console log user ID context.out.user.id and now obviously you can now imagine what you can do here. You can now do find many where ID is context.out.user.id so this way you can only query database users which belong to the currently logged in ID right let's actually do that let's query all accounts that belong to the currently logged in user ID this way you don't even need the console log you will just logically see whether it works or doesn't so let's go ahead now and go inside of app folder page dot DSX here and let go ahead and do data here and I think we can just do a wait is it color yes it is color dot get users and let go ahead and do data here and i think we can just do a wait is it color yes it is color dot get users and let go ahead and do json stringify data let me refresh this uh looks like this is now empty uh get users am i doing this correctly users user find many okay user works maybe the accounts are just empty but basically since i'm logged in you can see that i can successfully now fetch this user here right i can successfully fetch my user but let's just quickly do the following let me just add let me add this in a div let me add null to here like so uh flex call gap y6 i'm just adding some things so they are spacious now let's add a button log out uh on click let me see can i do i can't do log out from here because this is a server component uh okay let me try and think of something real quick here Inside of the app folder, just for fun, create logout.tsx. Mark it as use client. Import out client. Export const logout button. Return a button. Logout. On click of the button. Simply call out client.signout. the reason we need to do this is because we are playing with server components and client components and server components cannot do on click in the button so we need to do it like this by calling out client instead of a use client here so now just render logout button from .slash logout file there we go so you can see that now this is a protected server component and I'm logged in So if I click log out now, let's see, am I logged out? Maybe I have to refresh. But yeah, you can see that now I'm redirected here, right? But what if I forgot to do this? What if this breaks? What would happen now? So previously, maybe a better example is to not have anything here like this. Let's change this to base procedure. and so let's purposely yes forget this and let's purposely bring the base procedure back and remove anything from the find many if you go to localhost 3000 now you will have access to the entire data so this is what i'm talking about you shouldn't rely on this this shouldn't be your last level of security. Instead, your data access layer should use the protected procedure. And this way, you get the error. So when I tell you that I'm doing this a way to require out for user experience, this is what I mean. I mean to the fact that the user won't even see the error, I will just redirect the user away from the error. But in case this ever fails, I don't want my data to leak and it won't because my data access layer my trpc is going to be using the protected procedure in places where it needs to use the protected procedure that's what I was trying to explain to you previously that's why I emphasized on creating the data access layer because we're building a production ready app here we're not building a toy right so I want you to understand what a data access layer is, why tRPC is so useful in this project, and the difference between using auth protection checks within server components to redirect, between middlewares to redirect, layouts to redirect, and in actual data access layer to protect your data. That is the difference. Alright, I think that's enough for this chapter now and I'm pretty sure I managed to get my point across. I hope this made it clearer for you about what we're doing and why we're doing it. So yeah, you should be able to log in. You should be able to register. You should be able to log out. Your root page should redirect you to the out page. And even if this breaks, if you remove this, you should still get the error because you're using the protected procedure. so only once you actually log in and click log in here should you actually see the data and yes this logout doesn't work instantly so the way you can fix this is you don't have to do it now but basically sign out here accepts I think on success it accepts fetch options and then on success here just to demonstrate to you const router use router from next navigation and let's just do router.push login so if I try now antoniomail.com 1 2 3 4 5 6 7 8 and click login and click log out here still not working okay never mind I mean never mind obviously we will make sure this works but it's not really what I'm trying to show in this exact segment. I think it's because we are rendering a server component here so maybe I need to revalidate the server component first. I'm not really sure. I have to do something but I can't really think of it right now. What's important is that it actually clears the cookies once I click logout. And then when I try again after I refresh you can see that it's no longer allowing me to do so. So yes, obviously we will create a very reliable logout which redirects the user. But for now, this is enough. Perfect. So let me go ahead and check this. So yes, we just added out procedures here. And now let's push to GitHub. So 04 authentication. I'm going to go ahead and create a new branch. 04 authentication. I'm going to go inside of my source control. As you can see, I have 18 uncommitted files. So package lock, package, a schema, migration file. I removed the client file. Layout our little logout client button pages where I added mostly the ALF protectors, the important route handler for better ALF, login form, register form, important ALF client util, ALF utils, which I use to redirect the user away. alf.ts the most important util for better alf and then some modifications in the init where I added protected procedures and finally some routers where I used the protected procedures so that's the 18 files that we did. Let's go ahead and click stage all changes and let me name this commit zero for authentication. Let me go ahead and hit commit and let me hit publish branch and once this is published I'm going to go inside of my github repository here I will open a new pull request and now let's go ahead and review our changes keep in mind that a lot of things that we just did are of course just for demo so code rabbit will definitely have a lot of comments and a lot of changes that we have to do but that's okay we will change them in the following chapters what we did in this chapter was to demonstrate how we're going to use ALT and the dangers that can happen and the data access layer protection that we are going to do to fix it. And here we have the summary. New features, email password authentication enabled with session management. New login and sign up pages with form validation and redirects. ALT API route added, homepage now requires sign in, logout button to end session and return to login, toast notifications for out success or errors. Homepage content now loads server side after authentication. And we added authentication library dependency. As always, in here we have file by file summary, but what I'm really interested in are the sequence diagrams. And we don't have to go through the entire sequence diagram, of course, but I just want to bring your attention to something. So this is what I was talking about. You can see that when the user heads to the get route, we do all kinds of require out and some headers. And if we don't have the session, we redirect. Otherwise, we call get users dot query. But this is only the first layer of our authentication protection. The actual layer of our protection is the trpc get users protected procedure. So the last layer before we actually call Prisma, which has direct access to our database, is fully protected. That's what's important, right? So our app would be equally as protected as if we didn't add none of the redirects as well, either. If we didn't add any redirects in the first place, our app will be equally as protected because we protected our data access layer. That's what I was trying to say, right? I hope I managed to explain that well to you, right? And again, super impressed by how CodeRabbit managed to understand the entire thing that's going on with our app here. In here, we have a sequence diagram explaining how login works and how register form works, but I think we already understand it's a simple form submit, which then creates a new user in the database. In here, we have some comments on our migrations, which we don't really have to modify because these are just development migrations. Obviously, they are with issues because I just deleted the old user and added a new one without caring for migration at all. I just removed all records from my database. That's what it's warning me about. In here, I think it works perfectly fine with just updated ads. So I disagree with this comment right here. And since better out is, we use the migration script CLI to add this schema and better out works fine with it. So that's why I'm not going to modify this at all. I'm going to leave it as it is. In here, obviously some very obvious risks here by just stringifying my data. So we are not going to take that seriously right now because we will remove that In here it recommends increasing our password security by increasing the length the minimum length of the password So definitely something we have to do And in here, it actually noticed a typo. I typed unauthorized instead of unauthorized. So definitely have to fix that as well. And yes, we also have Google and GitHub to add. Those are mostly OAuth configurations more than anything else. But we will do that, of course. I will see if it will be in the exact next chapter or maybe later on. As I said, there's not much code we have to do here. It's just configuration and working with the third-party providers. So for now, let's go ahead and merge this pull request. I'm satisfied with it. And once we've merged it, let's go ahead and go back in here, select main. Make sure you hit the synchronize changes button. Click OK. And then go inside of your source control. once you've merged it go inside of graph and just confirm that it looks like this if you are following along with me regarding the git workflow so we detached 404 authentication and then we merged it back inside of the main branch so now you should have login and sign up inside of your main branch here perfect i believe that marks the end of this chapter let me just see it does there we go. We published to GitHub, we created a new branch, new PR, and we reviewed and merged. Amazing, amazing job and see you in the next one. In this chapter, we're going to focus on improving the look of our current app. We're going to apply a new theme, we're going to improve our screens and how they look, and we're going to add some logos. So let's start by applying the new theme. You can apply the new theme by using TweakCN. You can use the link on the screen to visit the website. It's an absolutely amazing website, which allows you to customize your ChatCN theme in great detail. And once you're satisfied, you can very simply hit Code. And in here, you can find the CSS that you need to copy to add to your app. Or you can even use their CLI interface. Now, if you want to have the exact same theme as me, I've prepared a solution for you as well. I have made a public repository here where you can copy my exact globals.css. So let's go ahead and copy this. So this entire file, you can use the link on the screen to access it. Or of course, generate your own using the website I just showed you. And let's go inside of source app folder, globals.css, and simply replace everything inside with the new content. Now, you will see two things that aren't exactly part of TweakCN here. So everything from the start all the way to, I would say, right here is TweakCN. But this is my custom code that I have added. it basically enables the pointer cursor for all buttons, which has been removed as of Tailwind version 4. I simply prefer my buttons to have a pointer cursor. And this will come in handy later when we add React Flow. So no really point in explaining this now because we don't really have React Flow installed at all. So you can find this in my public repository here. Simply find the globals.css file using the link on the screen and just copy the entire file and paste it here. That's the first thing I want you to do. The second thing I want you to do is to go inside of images and copy these three images and go ahead and add it inside of your public folder here. So inside of the public folder, we now have file, globe, next, the cell, and window. Now let's add these three here as well. So here we have, I've added logo, Google, and GitHub. So make sure you have GitHub, Google, and logo here. And I would like to separate this a little bit by just adding a folder called logos. And then let's move our Google and GitHub inside of that, like that. and you can even move the main app logo inside like that so it doesn't merge with all the other ones that we have already had inside perfect so if you're wondering where did i get the logo from it's from twixian so let me go ahead here you can use the link on the screen and in here you can find a bunch of interesting logos. Basically, Logo Ipsum is this amazing website where you can find placeholder logos for your projects. So you can just click to copy and you will copy entire SVG. Somewhere down here, I found this exact logo. Not sure where it is. Can't find it now. But basically, I mean, this is an amazing resource of logos. and I would highly recommend that you visit to find some interesting logos here. So now that we have the logo and that we have GitHub and that we have Google, let's go ahead and modify our app a bit. So I'm going to go ahead and do npm run dev and immediately you should see some changes. So let's go ahead and refresh our localhost 3000. I'm specifically on the login page. So immediately you've noticed that we now change the button color to an orange. So that's our new theme. Another thing that has changed is border radius has been reduced by a bit. So it looks a bit more blocky. I prefer it this way. You can, of course, do it some other way. So now let's go ahead inside of the login-form component, which we were previously developing. And in here we have two buttons for Google and GitHub. We didn't develop those functions yet. We will do that later. But now let's just make it look a little bit better. So above the continue with GitHub text, add an image. We should already have image imported from next image. And give it a source forward slash logos forward slash github.svg. And now let's go ahead and give it a width of 20 and a height of 20 as well. and let's see what is with my image why is it getting an error oh we are missing an alt property alt here is going to be github there we go continue with github and now let's go ahead and copy this image and let's do the same for google and just change this to google.svg and change this to be google there we go continue with github and continue with google here's how the code looks when it's not totally collapsed. There we go. And now let's go ahead and do something else. Right now, no matter how far I zoom out, this always kind of fills the entire area. We don't want that, right? So what we're going to do is, well, first, let's also add the image thing to the register form, right? So let's go inside of the register form. And let's just do the same thing just so we're on the same page for both of those there we go github and then google and change these to google and change these to logos google svg so now both sign up and login routes should have the icons and look better so now let's go ahead inside of source app folder alph login page dot tsx and make sure you are on the login page here as well so what we're going to do now is we're going to make this look better so let's give this a class name of background color muted let's go ahead and give it a flex a minimum height of svh flex column justify center gap 6 padding of 6 and md padding of 10 now let's go ahead and add another div here and let's give it a class name flex full width maximum width of small flex call and gap six then let's add a link property which we can import from next link like this Go ahead and give it an href to a forward slash. And inside, render an image from next image with a source of logos, logo.svg, an alt of node base, width of 30, and height of 30. Like that. and give the link property a class name flex items center gap to self center and font medium and after the image render node base just like that so now let's go ahead and zoom out a bit to see looks good but something's obviously missing here so let me just check what it is here so minimum height of svh it looks like we are missing the full width here so can i do minimum width of svw or maybe screen. Alright, looks like there is a slight issue here. Let me just check why. I think it's because I forgot to add items center here. There we go. So I forgot to add items center in the first div. And now once I've done that, the login screen looks much better, both on mobile and on desktop view. But there's a problem with this code. The problem is I would have to write the exact same code in the signup page for it to look the same. Well, there is a way we can reuse this route group. Previously, I told you that these route groups are only good for organizing your content. But they also good for sharing layout files So inside of that alt folder create a layout file This is another reserved file name in Next So let go ahead and define the layout like this And let's go ahead and define children react react node as the prop. And let's go ahead and simply return, oops, we already do, children. and just by saving the file and of course doing an export default there we go nothing changes really but now we're going to change it so go inside of login and let's copy this entire thing all the way to the login form and now go ahead and render it here and then below this render the children and then close the two encapsulating divs. Now let's go ahead and just add these few imports. So next link and next image. Perfect. Now let's go back instead of login and let's remove all of these entirely. So we can just do return login form and remove the unused imports. And let's go inside of page signup and let's do the same thing, just simplify these pages. There we go. So what we've achieved now is a layout file which is shared across all folders or all routes within this route group. That's the power of layouts. So now both our sign-in and our login routes share the same styles. And may I say, our app now looks much better. Great. And I think that that's all I wanted to do for this chapter. I wanted to keep it short, simply because we had some complicated chapters before us. But let's also improve our structure just a bit. So instead of features, we have auth components. So let's also add auth-layout.tsx here. This way, we are only using the app folder as the actual kind of router and business logic. We don't really care about styling within the app folder. For that, we're going to be using this. So let's just copy the entire thing, actually, except the children, and let's do export const auth layout. like this. Render the children, close the div. Close the div. You can copy the props from here. And you can copy the two imports. Like this. And now we have the outlayout separately in a named component and then instead what we can just do is just outlayout. And this matches our code consistency a bit more. I'll show you why. So let's remove the unused imports. So layout is a reserved file name, which is located inside of the app folder and where it structurally needs to be to work for route access, right? But where we actually define the component with styles and images and everything doesn't need to be here. Instead, let's keep it always in one place, in features, out. so this way same thing happens in login and sign up pages we develop them here because they need to be here structurally for this route to work right but where we actually develop the component doesn't have to be inside of this out folder it can all be together in features out components so i feel like that's a better solution right because now whenever you're wondering oh how do i modify something from out very simply just go inside of features out components all of them are here. You don't have to worry about visiting them here. Because the way they are organized here is according to the router. But here, it's according to their feature. Thus, we call this folder features. I think that's a better structure. Feel free to write in the comments what you think. Excellent. Amazing, amazing job. I think this marks the end of this simpler chapter that we've had. And yes, we still have to develop these two. But I think I might focus on some other things for now simply because these take a lot of time, but they are mostly just configuration setup. So we might actually do this closer to deployment because that's when we are going to start doing this configuration. I'll see. Maybe I'll do it in the next chapter if I change my mind. But as of now, I think we all want to see something interesting, like some background jobs, which are closer to what the goal of this project is, which are automation, right? So let's go ahead and see if that's what we wanted. We applied a new theme, we improved out screens, and we added logos. Amazing. And now let's push this to GitHub. So 05 theme and styling. So I'm going to create a new branch, 05 theme styling. And let's go ahead and open our changes. Let's go ahead and stage all changes. 05 theme and styling. Let's commit and let's publish this new branch that we have. And once we have published the branch, let's go ahead inside of our GitHub here. Let's open a new pull request, 05 theme and styling. and since this was a very very simple pull request we don't really have to review anything here we already know what's going to happen in this case so we added this new logos we added the reusable ALT layout and now it's written here in the features so it matches the structural consistency of all other ALT components and we just modify globals we added this some custom things. I didn't really think there's anything important here. The only thing that can be mentioned here is that these buttons still don't do anything, right? You still can't log in or register with GitHub or Google. Other than that, I think we can merge this. Obviously, very thankful for the help CodeRabbit wants to do, but this was a fairly simple pull request. So let's rather use CodeRabbit review on something complicated, which will be coming very soon. So let's merge this pull request. And once you have merged this request, as always, let's go ahead and let's just change our branch back. So head here, select the main branch, and synchronize the changes to make sure that you're up to date. And once you have synchronized your changes, just go ahead inside of source control, graph, and confirm that you have the same view as me. Let me just show you. So I detached into 0.5 to add some theme and styling and then I merged it back into main. And you should see that here. There we go. Amazing, amazing job. Our app now looks much better and we have a new theme, meaning automatically things we build from now on will start to look good. Great. I believe that marks the end of this chapter. We pushed to GitHub. We created a new branch. We created a pull request and we did a personal review and merge simply because this was super simple and we knew exactly what all the changes are. Amazing job and see you in the next one. In this chapter, we're going to get familiar with the most important concept for building an automation app and that is background jobs. Let's take a look at a few examples I've prepared to show you why we need them. So we all know a normal networking example. For example, the user clicks login, we send a network request, and we get an instant response. Success or fail. But what if we had a more complex example? For example, an AI summary generator. When the user clicks generate summary, we send a network request. And during that time, the backend generates summary. Now, the way backend do this is a bit complex. Imagine it's a summary of a YouTube video. Imagine it's a summary of my YouTube video, which lasts for 12 hours. We have to include three external services for this. First, we have to fetch the YouTube video. Then we have to transcribe the YouTube video. and after that we have to send the transcription to some AI provider to summarize it. During those three external services, many things can happen while the user waits. Maybe something can time out. Maybe the connection can get lost between any of those three services. Or maybe the user itself can break the connection. All of those things can cause the user to never get the result. because of that we're going to think of something called background jobs this time when the user clicks on generate summary we're going to send the network request as usual but instead of immediately executing it we're just going to queue a background job and once the background job is queued we're going to send to the user that the summary is being generated and at that point the user is free to close the tab or move on. So they will instantly see a response the same way that they did in the first example, right? Instant response, success, right? We successfully started a background job and then the user can move on to do other things. This is very important for our automation app because imagine the user had to wait to complete all of those complex services. If you remember in the intro video in the demo, I demonstrated how we can transcribe something with OpenAI and then send it to Slack and then send it to Discord. Imagine if the user had to wait for all of those things and imagine if something timed out or if something failed. Well, because of that, we are implementing background jobs, which will simply notify the user when something is finished. So let's go ahead and try and do a simple demonstration of this. I'm going to go ahead and head inside of my Prisma, and I'm going to go inside of Schema Prisma. And then at the end,\nHere I'm going to create a new model, which we're going to need anyway, workflows. Let's actually call it just workflow since all the other models are single as well. So account verification workflow. Let's give it an ID, which is a type of string. Give it an ID decorator and a default value of CUID. And then let's give it a name with a value of string. So let's keep it very simple for now. No need to do anything further. And then let's go ahead and do npxprisma migrate dev. And for the migration name, we can simply do workflows table. There we go. Now restart your server if you haven't already. And you can go to localhost 3000. The last thing we did here was the login screen and the protected server component. So if you are logged out, you should be seeing the login screen. And then you can just log in. I use this simple combination of email and 12345678 password. And in here, all I do is I fetch, using a protected procedure, I fetch the users in the database. So now I'm going to slightly modify it with my new Prisma model. Instead of source drpc routers, I'm going to go ahead and change this from get users to get workflows. And I'm going to change the Prisma user find many to Prisma workflow find many. We can ignore the error for now. We will resolve that in a moment. Let's actually go ahead and add create workflow procedure. Let's also do a protected procedure. But instead of query, let's add a mutation. And let's go ahead and we don't have to extract the context really. Let's just open a function and let's return prisma workflow create passing the data name test workflow. As simple as this. Now that we have that ready, you can remove the base procedure from here since we're not using it and go inside of page.tsx in here where we have the error because we are calling get users which no longer exists and let's simplify this a little bit. Let's mark this as use client. Let's remove asynchronous from here. Remove await require out and instead of getting the data like this let's go ahead and this time get it by using use query from tan stack react query and let's get trpc from use trpc from the trpc client and then in here we can pass trpc get workflows query options there we go and now in here after a brief second of loading you should just see an empty array so now what I'm going to do is I'm going to add a constant create and I'm going to add use mutation from tan stack react query drpc create workflow mutation options and then I'm going to add a button here create workflow I'm going to give it on click to be create dot mutate and let's do this to fix the type errors and then I'm also going to add a disabled prop when create is pending. So this way I can track exactly how much time has passed. So right now, if I go ahead and click, and actually while we are here, we can also do one cool thing. In the mutation options, go ahead and open an object on success. Let's go ahead and do, let me just get the query client, use query client from DanStack React query. query client dot invalidate queries trpc dot get workflows dot query options and I think we can also pass this like this or maybe not okay I think this should work I think we call use query client so now immediately when you click create workflow it will update because we invalidated the queries here so we fetch them again. So this way you don't have to refresh your page. So you can see it works pretty instantly, right? The moment I click create workflow, a new one is created. But what if we had a more complicated example? What if inside of my app router here in the create workflow, instead of having instant response, I had to communicate with external services. So for example, I will mark this as an asynchronous function. And then in here, I'm first going to fetch the video. Imagine this is the transcription process, right? So first I fetch the video and that will last 5,000 milliseconds or five seconds. After that, I'm going to transcribe the video. So that's another five seconds. And then I'm going to send the transcription to OpenAI, for example, another five seconds. So let's see how this looks now. I'm going to go ahead and click create workflow. And you can already see that this isn't a nice experience. The user now has to wait for 15 seconds before seeing any kind of feedback. The user doesn't know if they can log out. The user doesn't know if they can refresh the page. They just have to wait for 15 seconds until this completes. And now imagine if something happened. What if this step here failed? But we already used some resources to fetch the video. The user would have to start the entire process again. Because of this, we're going to implement background jobs. This way, if a single step fails, we can easily retry it without having to start the entire job again. And that's just one of the many advantages using background jobs gives us. So, let's go ahead and add ingest to try and improve this problem. So, you can use the link on the screen to head to ingest. and if you want to you can create an account here but you can also create the entire development environment without creating an account which I think is absolutely amazing. I love when apps gives us an option to do that. So let's go ahead and create Next.js and let's go ahead and install ingest. So I'm going to add ingest here and I'm going to show you which version I'm using simply so you are aware and if you want to use the same version as me. So if you want to, you don't have to install it immediately. You can wait till I show you the version and then you will see where you stand. So let me open my package adjacent here. Let me search for ingest and here it is 3.44.1. There we go. And once we have ingest, we also need to run our ingest CLI. and for this I will also just show you the version real quick. So ingest CLI version you can see it's 1.12.1 and I think that this will either return the version or maybe throw an error if the version flag doesn't exist. Looks like it exists so yes this is the full version that I'm using. So yes now you can do npx ingest CLI. Is that the correct one? it's not ingest dash cli latest and dev what this will do is it will basically spin up an instance of ingest locally for you and you can access it in localhost 8288 and in here you will see all the runs that are happening in the background because that's the only way you can actually keep track of them. So let's go ahead and make sure that you have both that running and npm run dev running. And then let's actually set up ingest following their documentation here. So we just set up the ingest dev server. And we've opened dev server here. Now let's create the ingest client. So very simple just two lines and let's go ahead and add that inside of source let's create a folder called ingest and then inside of here client.ds as simple as that and let's import ingest from our newly installed ingest package and let's export const ingest with new ingest and the name can be node base. There we go. We have our client here. Now let's go ahead and let's create the ingest route. So this is something that we've been doing for pretty much all services that we've added here. So instead of app folder API, we did it for auth and drpc. And now let's also do it for ingest like this and let's go ahead and just create a route.ds inside and let's copy this and let's paste it and let's see what it's about so we are importing serve from ingest forward slash next and then we just need to add our ingest client yes you can replace that relative import with an alias to kind of teleport you to the root of your project so your imports look nicer we currently have no functions so we can't pass anything here but you already recognize this right we are exporting get post and put endpoints using this route.ds the same thing we kind of did here right we are exporting get and post and we did it here right so it's a common pattern that we are doing here. So just make sure you have added that. And then let's write our first ingest function. So I'm going to go inside of source ingest. So where your client.ts is, and just add functions.ts. And let's go ahead and copy this and paste it. And let's see what we have here. So we are importing ingest from its neighbor client, and we export a function called hello world We use ingest function to create it We give it an ID and very importantly we give it an event The event will be used to execute this function later on It does a very simple thing It sleeps for one second and then it returns a message using some payload that we can pass. You can see that the payload can be anything. I can change this to name. I can change this to surname but let's keep it email as their original example. once we have created hello world following their example we have to add that function to serve so let's go ahead back inside of our app folder api ingest route and inside of the functions here let's add hello world from ingest functions just like that and the moment you do that if you have your ingest cli dev running and if you have your npm running you will also see a bunch of requests to ingest and in the beginning you will see a bunch of invalid requests here because at this point something I mean something this the CLI was searching through our app for ingest endpoint it tried Netlify it tried this basically until it found the working route and then it just sticks to that app which works and once you have hello world you can now go to your let me just find it here it is ingest dev server localhost 8288 and when you click in functions you will now see the hello world here and what you can do now is you can actually invoke it from here and you can pass in the object like this perhaps it will be empty for you this is because i already use the ingest dev server so yes feel free to just write an object make sure you're using quotes for your fields here and then just pass in the email for example antonio at mail.com. And make sure to not use any commas. I think that's an invalid JSON. Yes. And click invoke function. And you will see that the function is now running. And it finished pretty quickly. And what happened was, is exactly what we said will happen. It slept for a second, and then it finished with a message. Hello, Antonio at mail.com. So let me just open the docs simply because I have habit of not finishing through the docs because I'm excited to show you how to use it but I'm pretty sure that's what the function uses you here I think then it goes into some other examples which we are going to encounter anyway yes we are now going to trigger from code so yeah let's go ahead and do that here so instead of triggering it by clicking the invoke button let's go inside of source trpc routers underscore app and let's go ahead and focus on this here so now instead of doing these three things here let's go ahead and remove it and instead let's do await ingest from ingest client.send name test forward slash hello.world data email and well we could technically extract the email from here but it really doesn't matter we can just do antoniomahol.com or whatever you want we are now just trying to execute this through a trpc function so await ingest.send and the name here matters because it needs to be the same as the event. So make sure you didn't misspell it. You can copy it and you can paste it here directly if you are not sure. So let's try it out now. So I'm going to go ahead back here. I will click create workflow. And now, okay, it immediately finished. So let me try and make this a little bit longer. So wait for a moment. I'm going to change this to 10 seconds. I'm going to refresh and I'm going to click create workflow again. And now you can see that it's sleeping for 10 seconds. But the cool thing is this is not pending, right? So it did exactly what we planned to do. Let me show you. When the user clicks a button, we send a network request, but we immediately send to the user, okay, we started the background job. you are no longer disabled right your button is no longer disabled as it's pending you can move on you can do something else we are doing this in the background it makes no difference if you wait here or go somewhere else and we're going to finish the background job and notify you when we are done that's exactly what's happening here so now in here you can imagine that this is fetching the YouTube video, right? And then in here we are transcribing the video. And then in here we are sending transcription to AI, right? So let me just go ahead and change this to five seconds here. And we can even change this then. We no longer have to do this here. Instead, we can do await step.run create workflow like this and let's go ahead and return Prisma, which we can import inside of this ingest environment. So Prisma dot workflow dot create data name workflow from ingest. so we have basically now abstracted this entire function which no longer needs to return the prisma workflow instead it can just return success true and a message job well with cued not quered sorry uh all right uh and now let's go ahead and try it out so we have the same example that we had in the beginning of our project right but this time you can see it immediately returns this back right so this is no longer blocked and you can see that the jobs are happening right so this is waiting for a moment then this is waiting for a moment and if this step failed, we could easily, as an admin, see why it failed here, and we can even retry it, right? So I should probably call this transcribing, I should call this fetching, and I should call this sending to AI. But basically, if any of these steps fails, I can always control, you know, the retries. For example, five retries, right? Or if I want all steps to always work, I can set zero retries. So that's what's cool about this background jobs besides the obvious, right? In case transcription step fails, we can very easily, you know, just retry that step. Maybe it's timed out. Maybe their server broke at right this moment. But the thing is, we are going to delay next retry for twice amount of time well i just said something that i don't know if it's true but we definitely delay the next retry so we give it time to not hit any rate limits or things like that right that's another cool thing about the ingest so let's go ahead and try again and this time let's make it even more obvious by going to page.tsx here and on success we no longer have to do this instead let's just do toast from sonar dot success job queued import toast from sonar and now you will get a message job queued and now the user knows okay so whatever i just did is happening in the background right i don't have to worry about it because right now it's fetching the video for five seconds, then it's transcribing my video, and now it's sending it to OpenAI. And finally, in a real world example, you know, it would have all the data and it would create the workflow, right? That's what I'm trying to demonstrate here. And you can see that, well, now we have to refresh to see the new model, but don't worry, later we are going to connect Ingest to their real-time service to show to the user in real time when the job is finished, when it's in progress or if the job failed. So that's what's absolutely amazing about Ingest. And I believe that that is a very good introduction to background jobs, which are a very, very crucial concept for us here. So let's see if we did everything we intended to do. We set up Ingest, we created a background jobs, but we didn't add this. So the reason I wanted to add this is definitely not required. I just think it's cool. So you can see how I need to have two terminals. And every time I start this project, I have to remember to run both this and I have to remember to run this. So if you're interested, you can install this package. It's called mprox, like multiple processes. You can also use concurrently. But the thing I really like about this is that it will show you the processes that are running individually. And you can see if any of them fail. This is obviously not required. It's for running multiple commands in your development mode. But it's very easy to install. You can just use npm install global multiple processes. So I can go ahead and just add this for example. right but since this will be kind of in your app perhaps it will be better to install this as a dev dependency and then all of your collaborators can do that as well so let me show you my package json here and procs there we go 0.7.3 inside of my dev dependencies here and now that you have this all you have to do really is create a simple mprox dot I'm never sure how to pronounce this extension. Let me just show you. So in the root of your file, create mprox dot y-a-m-l processes and let's add ingest cmd npm run ingest dev. next cnd npm run dev again this is completely optional you don have to do this right it already working for you you can run both of them at the same time But if you want to do it with the same command you can use concurrently or you can use this This is a new thing that I found and I really like it And now let's go inside of our package JSON. And let's go inside of scripts here. So I'm now going to add ingest dev. And I'm going to add ingest.cli dev. and I'm going to add dev all and in here I'm just going to do mprox. And let's also install ingest CLI as a dev dependency the same way we did before. So I will actually shut down this now and I will do npm install inside of my dev dependencies ingest CLI. this way whoever you know ends up working on your project will have the exact dependencies that you had like this mprox and ingest cli and if i've done this correctly and i think i did when you run dev all it should find its configuration file and it should run both ingest and npm run dev at the same time so just make sure you call this ingest dev and then it will just make sure you didn't misspell it here right so let's try npm run dev all and there we go you can see how it starts both ingest and next js now keep in mind that it's a little bit different so for example if you want to quit this you have to press the letter q ctrl c will not work so you have to press the letter q and then it will shut down everything same thing if you want to copy something from here if i want to copy this you can see i'm in copy mode now and i have to press the letter c to copy right so when i press c that copies it ctrl c or command c will not copy it's just the letter c that will copy it you can use your mouse to select the process or you can go up and down and you can also do a cool thing you can once your process is selected you can press the letter R and that will restart it. You can see that now I just restarted Next.js or if I want to restart my ingest I can just press R. I personally think it's cool if you don't like it you don't have to use it I don't know but I just wanted to show you this in case you don't so you don't get surprised if in the middle of tutorial you see me having this multiple processes thing. It's inspired definitely by the turbo process manager which uses dmux I think but setting up turbo might be a little bit of an overkill for this project I don't know I'll have to explore it a little bit more I usually use it only when I need monorepo and setting up dmux is not as simple as doing an npm install so if this is not working for you no problem just continue with the tutorial but if it is I think it will kind of help you because we will have some more processes to run synchronously and that will be like NGROC, our local tunnel. So it will be very easy to have all of them here together. Great, so if it works for you, great. If it doesn't, no need to use it. And I think that now that marks the end of everything we wanted to do. So let's go ahead and let's commit this. So 06 background jobs. I'm going to go ahead here and I will create a new branch, 06 background jobs. and now let's go ahead and stage all 10 changes that we have. 06 background jobs and let's click commit and let's click publish branch. And now let's go ahead to our repository and as always let's open a pull request and let's review our changes. And here we have the summary by CodeRabbit. We added a workflows list with client-side loading. We introduced a create workflow button with success notification and disabled state while processing. Background tasks triggered on creation to handle processing. We converted the main page to client-side. We only did this, so it's easier for us to execute the functions. We added local tooling to run multiple development processes concurrently, exactly, and we introduced new development runtime dependencies, which is referring to ingest. So what I'm excited about is the sequence diagram. You've probably guessed it already. So let's go ahead and take a look. When the user clicks create workflow, what we do is we call a mutation.createworkflow. The TRPC router receives that. And once it sends the event to test hello world, the ingest API acknowledges it. And we simply send back to the user the success message. and if you remember this is exactly what i planned on doing let me just find my example right here so a long-running task example with background jobs the user clicks generate summary we send a network request we queue the background job and we simply acknowledge the background job the summary is being generated that's what this step is user clicks the button we send the network request we queue the background job and we send to the user a success message like hey that's it that's all you have to worry about but what's actually happening is that something's going on in the background while the user can do whatever they want in our case we just sleep for 15 seconds but we are pretending that this is fetching the video transcribing the video and sending the transcription to the ai and then we create some record which obviously makes no sense but in the real example this would use some data from these three steps that we just did. That's basically what we will do in the future and that's exactly the flow that CodeRabbit managed to understand every time I see this and every time I'm impressed by how well it understands the code that we are writing. Here we have a couple of actionable comments but we will not do anything regarding that simply because we just modified a bunch of our files just to demo right so none of this is really going to stay here we're going to change our file structure a lot for this page.tsx as well as the actual functions are not going to look like this so it makes no sense to change them because they are not real right we just use them as an example so amazing amazing job let's go ahead and merge this pull request here and let's go ahead and see if that's all we had to do. I believe that marks the end of this chapter. It does amazing, amazing job and see you in the next chapter and yes before we of course end the chapter I almost forgot we have to go back to our main branch and after your main branch here make sure to synchronize the changes, click okay and then head inside of your source control your graph and in here you should see 06 background jobs being detached from the main branch and then merged back here and for your sanity check make sure you are on the main branch and that you can see for example mprox configuration file or ingest file that means everything's fine and that marks the end of this chapter. Amazing job and see you in the next one. In the previous chapter, we've created an infrastructure which allows us to run long-running tasks within our project. And in this chapter, we're going to put that to test by adding AI functions. In order to do that, we first have to choose an AI provider. Using the link on the screen, you can visit AI SDK. Go ahead and head into the providers section. And in here, you can find a list of all compatible providers. You can see that it ranges from popular ones like OpenAI, Anthropic, Google, which is Gemini, DeepSeek, all the way to lesser known ones. And now I've extracted average pricing for those, simply so you know if you have the budget or not. Gemini, which is what I will be teaching you how to use in this chapter is completely free, at least at the moment of me recording this video. OpenAI and TheTropic, which are by far the best and most popular models, require a minimum of five dollars, which will be more than enough to complete this project five times. But in order to make this easy for everyone to follow, I will be using Gemini. So let's go ahead and start with setting up AI SDK. So I'm going to go ahead here and let me try and go back here and first things first let's do npm install AI. So you can see that I'm using AI SDK version 5. So I will simply do npm install AI and then I'm going to head inside of my package JSON here just to show you the exact version. Here it is AI 5.0.60. Once we have that, let's go ahead and let's add a provider. So in order to add a provider, you have to go inside of providers here and you have to find the one you use. If you're using OpenAI, you would simply select OpenAI provider, you would select your package manager and you would run the command. So in this example, we're going to be using, I'm never sure which one it is. Okay, we're going to be using Google Generative AI, which is Gemini. So let's go ahead and select NBM. Let's go ahead and copy this. And let's paste it inside. NBM install at AI-SDK forward slash Google. And then let me show you that as well. So you can see the exact version. So that's up here. Here it is, AI-SDK forward slash google 2.0.17 so that's the version that's compatible with my 5.0.60 version of AI. Now that we have that ready we have to go ahead and get an API key. So for whatever provider you choose you can very quickly just click on the hyperlink here and you will be redirected to their website and from there you can find the API keys. So for Google Generative AI, let's go ahead and click here which will take us to ai.google.dev. I will have the link on the screen so it's easier to find here. One quick tip for you. The first time I tried accessing this website, I went into Explore Models in Google AI Studio and I was actually kicked out from here and the reason was I didn verify my age You need to be at least 18 years old to access Google AI Studio So in case you are wondering why are you getting kicked out from Google AI Studio, it's probably because you have to verify your age. So just go to your Google account, go into settings and find a way to verified. But if you can access this screen, most likely everything's completely fine. And right here at the bottom, you can actually see get API key. So I'm going to go ahead and click create API key button. And I'm going to call this node base. And now you have to either create a project or choose a project. So I don't have any. So I will click create project and I We call this Node-Base Project. Let's go ahead and wait a second for this to be created. And then hopefully we will be able to select it right here. Here it is, Node-Base Project. Node-Base Create Key. And once the key has been created, let's go ahead and click Copy API Key. And you can see I am on the free tier. I don't have the credit card added. Hopefully, that's the same for you. And let's go inside of .environment here. And let's go ahead and do Google Generative AI. And let's do Google or maybe Gemini Secret Key. And paste it inside. And now to find the actual proper name we should be using for this. I mean, it doesn't matter. You can, of course, use whatever name you want. but I just want to show you that if you actually go to AI SDK here, you will be able to find the exact value that they are expecting. And here it is. So API key, which is a prop, so you can always pass your custom name, but this is the one they are expecting. Google Generative AI API key. So I will use that instead. So Google Generative AI API key. Perfect. Now that we have that, let's go ahead and try and use it. So I'm going to go ahead inside of first just a normal trpc router to keep it simple. No need to mix it with ingest for now. And I will simply add a new route here. And let me go ahead and expand this a little bit. Let's call this test AI. It will be a protected procedure. it will be a mutation and inside of here let's go ahead and let's see what's the best way to do that I want to do this I want to do the generate text one so I will mark this as an asynchronous method here and I am going to import generate text and google from above so let me just add google from aisdk google and generate text from ai and then inside of here i'm very simply going to get that text here so let me just copy this and let me paste it here here it is text and let me just return text back so write a vegetarian lasagna recipe for four people you can change this of course to whatever you want i will stick with the original query here so we are using a model Gemini 2.5 flash depending on when you're watching this video perhaps they updated the documentation but I think that it's all type safe yes you can see exactly which ones they offer so let me just move it back to 2.5 flash I'm not really familiar with google models I guess this is okay now that I have that in my router I'm going to go inside of my source app folder page.tsx and I'm going to do const test AI use mutation trpc test AI dot mutation options and let's go ahead and use this test AI and let's just do a new button here with test AI text here on click it will simply call test AI dot mutate and just to be aware of how long it takes let's also disable if test AI is pending like this. Now let's go ahead and make sure our app is running. So you can do that using npm run dev. But just a quick reminder, in the previous chapter, we have also added dev all. But since we don't need ingest at the moment, npm run dev is fine. So let's go ahead to localhost 3000 here and in order to test this I think the best way is to actually open your network tab because I didn't see how else we can see the results really. So let me go ahead and go inside of my network tab here. Get workflows is failing maybe because I'm logged out. Let me go ahead and log out and let me go. Yeah since we removed the redirect so make sure that you go to login manually if you have your errors here it's probably because you are logged out so just make sure you log in again maybe the cookie or the session timed out or something right and let's click test ai and here we have test ai let me go ahead and you can see it takes a while right so something is happening now uh and we're actually going to get a much prettier view of what's happening once we connect this to ingest to a background job and you can see how useful background jobs are for ai queries you can see how long this took and we had no idea if it went successfully or not until we actually look here and here it is here's a delicious and hearty vegetarian lasagna recipe designed for four people focusing on fresh vegetables blah blah blah blah so it works we successfully connected Gemini to our project using a free API key but you can see the problem right when I click test AI it really does take a while right so this is a real world example of a long running task and this can very easily time out in production. If you deploy this on Vercel or Netlify and you don't use their streaming option, this kind of request will time out and your users will never actually get the result back. And what's worse, they will actually use up your tokens. So the perfect solution for this is to put them in a background job instead. So now we're going to do that. Let's go ahead. And in case you're struggling with this like logged in logged out thing you can also just make your life easier by going inside of routers app instead of using a protected procedure just use a base procedure and import base procedure and this way even if you're logged out so even if you're going incognito mode you will be able to test the ai function so now instead of running this here we need to abstract it instead of something like this. So let's do that. I'm going to go ahead inside of source and instead of ingest, and here are the functions. And now I'm going to delete everything in here like that. And I'm going to change this a little bit. So let's call this execute and let's call this execute as well. Or maybe let's just be a little more specific. So like execute.ai and id execute-ai. I don't know. I just want to make sure that we can do this. I think this might be the proper one. Maybe like that. Okay. and let's rename the hello world here to execute and once we have that let's go inside of our trpc API route my apologies not here not trpc ingest right here and let's replace hello world with execute function here it is so execute execute, you can close the tRPC route and functions here. Perfect. And now, instead of calling all of those wait elements, let's simply call the AI function. And now here, we can import create Google generative AI from AI SDK Google, and we can import generate text from AI. Now let's go ahead and let's define Google, right? So const Google is going to be create Google generative AI. And in here, you don't actually have to pass anything simply because it will work out of the box like this, because we are using Google generative AI API key name. So if you named it something else, in that case, you would have to pass that here and then process.environment, custom, Gemini, key, right? But since we are using this specific name and since it already worked through just a DRPC router, I'm fairly confident that it will work like this as well. And then in here, there's a cool thing that Ingest offers us. Instead of running the actual AI.generateText, what we can do here is we can extract the steps from await step.ai.wrap. And let's go ahead and call this Gemini generate text. As the second argument, let's pass generate text. And then as the third argument, let's open an object. And then in here, in this object, let's define the system to be you are a helpful assistant. and let's define the prompt from the user to be what is two plus two. Like this. And let's also add the actual model that we will be using, which is using Google. And inside of here, let's pass in the Gemini 2.5 flash. So let me collapse this so it's easier to look.\nand understand. So inside of our ai.wrap we have three props passed. First one is the name of our step. Second one is the function that we are wrapping which is generate text from our ai library and third one are the properties of this generate text from ai. So essentially we're doing the same thing that we just did inside of this route.ts routers instead of underscore app here. You can see it's kind of the same thing, but it is optimized for ingest. So it kind of becomes a step. It's abstracted in this way. And now that we do that, what we can actually do is we can just return the step here. And I think already we should see some results. Steps, whoops. And now let's just make sure that we actually fire this event, execute forward slash AI. So again, inside of our underscore app folder, let's go inside of test AI. and now here we're going to do the same thing. Await ingest.send and we are just going to send execute forward slash AI. Let me just confirm so it's name of the event. Here it is. Execute AI and no need to pass any data really. and yeah, we can just copy this so again, we just queue the job we're not going to make this long running task happen and let the user wait here with a disabled button not knowing how long it's going to take we're going to give them an instant response now in order to run this we also have to run our npx ingest CLI latest dev or you can use what we developed last time if it worked for you npm run dev all which will run both ingest and next js simultaneously so now i'm going to go to localhost 8288 in here i have my runs and i'm going to refresh this one more time and i will click test ai instantly i'm unblocked and in here you can see that we had a Gemini step and it was called let me see the full name Gemini generate text and the output here is so the content is what's two plus two and if we go all the way down here somewhere we should find the response I'm going to try and find it here if we can't find it I will very simply just change the code or maybe in the finalization step oh here it is 2 plus 2 equals 4 so it instantly responded I thought it was going to write here's your answer blah blah blah but it instantly responded that 2 plus 2 is 4 perfect so that's how we're going to run our AI jobs through background jobs so this way when someone just tries to execute an AI job, they are going to get an instant message like, hey, all right, that's in progress. Do whatever you want. We're going to handle this for you, right? So let's go ahead and try this once again. And now I'm going to go inside of my functions here. And let me just add a simple await sleep here. Await step.sleep. And let me see. I'm going to call this, I don't know, pretend. and then let's sleep for, let me just see, oh, five seconds. I think I can just do this. And let's go ahead and change our page.tsx here so that when we click on the test AI in here, in the mutation options, let's just copy the unsuccess and let's just say AI job queued. so you can now finally start to see how handy these background jobs are when we have a real world example so let's refresh let's click test ai instantly ai job is queued and in here you can see we have a pretend step and then we are starting the ai job so imagine in the pretend step we kind of fetched something or we communicated to some third service that's what's happening here And you can now see how useful this is going to be when this test AI button becomes execute workflow button. And then we are going to execute topologically all the nodes that are connected. That node can, for example, be fetch a website or fetch an API. And then use the result of that fetch and put it inside of an open AI request or a Gemini request. and after that, send it to Slack or Discord. You can see how long that would take within a normal network request. That would certainly time out on all providers. But if we offset that to a background job like Ingest, it is much more reliable, much easier to work with, and a much better experience for the user because look at this, instantly they get a response. All right, we got it. You can do whatever you want. we are going to handle this entire workflow for you don't worry that's what we are doing here and we can do a bunch of side effects we can do analytics we can do whatever we want during these background jobs and not to mention all of this can retry if gemini step fails we're just going to retry it right that's how useful all of these things are and now to finish the chapter Let's see, we selected Gemini, but I'm also going to show you how to add OpenAI and Anthropic, regardless if you have the API key or not. I would suggest that you follow along and simply let them error, right? You don't have to have them working. We also need to demonstrate errors somehow. So just integrate them together with me, regardless if you have the API key or not, and you are just going to have an error, right? For example, I don't have an Anthropic API key. I just have OpenAI. So my Anthropic is going to fail and my OpenAI is going to work. But it's useful for you to have the proper integration of all three of them because that will make it quite powerful to whoever you want to show this to, right? And because these are very popular and very good models, so it will be a shame because it's super simple to create them, right? So the way we do this is we go back to AI SDK, right? and we click on OpenAI, we select NPM and we install. So let me go ahead here. Let me install AI SDK OpenAI. You scroll a bit down and in here you can find OpenAI API key environment variable, which tells you the default that it's expecting. So let's go ahead and do OpenAI. Here it is. And let's do Anthropic already. and we're going to do the same thing. So, anthropic npm install, and let's find the variable, anthropic API key. Here it is. So, now I'm going to show you how you can obtain OpenAI API key, simply because I have it here. so you have to go to platform.openai.com you can see the link on the screen once you sign in here in your settings here make sure that in your billing here you have some balance so as i told you the minimum to add is five dollars i've been using this balance for the past three or four projects that i've done so you can see it's very cheap for what we are doing except if you don't you know, overuse it. And in here you have API keys. So you can see I already created one when I was developing this project. So just click create new secret key, node base. If you don't have a project, just select a default project and click create a secret key, copy it here, and then just paste it here. And the reason I'm telling you that you should add OpenAI and a thropping regardless if you have API keys or not, using, after you finish this project, you're not going to use your API keys. Your users will add their own API keys, right? So we are just doing this for demonstration purposes. For example, I'm going to demonstrate leaving Anthropic empty because I don't have that API key. But later when the project is finished for each of these products, for each of these nodes, your users will have to create new credentials. And in those credentials, they are going to enter these fields right here. So they will have to take care of the costs of their AI providers. So don't worry about that. And now let's go ahead and we don't have to create any new actions. We can just focus on this one AI job that we have here. So let me go ahead and do const open AI, create open AI I'm not sure which one it is let me just quickly check so it is create open AI from a ISDK open AI and it's opening I like this and let's do anthropic create anthropic and I'm 90% sure it's just anthropic create anthropic, as simple as that. And now that we have all three here, regardless, if you have the API key or not, let's go ahead and call this alias this to be Gemini steps, like that, and then copy this, paste it here, change this to be OpenAI steps, and use OpenAI, change this to be, I don't know, there are so many options. GPT-4, I guess, I don't know. And let copy it one more time Change this to be Anthropic steps And change this to Anthropic And oh this I don know Try to use some cheap ones. If that means anything to you. I think Opus is very expensive. So I'm going to use Sonnet. Maybe, I don't know. And let me just change this to be Anthropic generate text. And this will be OpenAI. generate text perfect and then here we will just return gemini steps open ai steps and anthropic steps and did i like this there we go so now we should have no errors at all and if we try to go back instead of our app here refresh and let me close this and click test ai you can now see we have this running and let's see what happens so what i expect to happen is the first two to succeed so gemini succeeded now we are doing the second one open ai succeeded and anthropic has failed because as you can see the api key header is required and you can see what's actually happening here it's retrying right so that's what i was trying to explain to you. Imagine this wasn't an API key issue. Imagine this was a rate limiting issue or a connectivity issue. If this was a normal network request, the user would have to do it all over again, regardless if they already spent the tokens for these two. But thanks to ingest, we can still save this by attempting to run Anthropic again. But since this is an obvious issue you can see it will just continue to fail in the second attempt and the third attempt will take even longer to try you can see they purposely added delay before trying again they don't want to get rate limited right so yes as expected open ai is i mean anthropic is failing because we didn't add the api key here so let's just click cancel for now so it doesn't run and yes this one is failed. For you, it might stop after OpenAI, right? But as I said, implement OpenAI and Anthropic with me simply because when you finish this project, it will be your users who will add API keys, not you. But we do need our API keys to actually test if this is working, of course, or to just test if it will fail. But the cool thing about using generate text from AI is you can see that it's the same everywhere. The only thing we modified is the model that we are using. So you can be pretty confident that if your free one is working, the Gemini one, all the other ones will work as well because this is the same API, right? I would expect this to work. I wouldn't even test it. I'm 99% certain that the Anthropic one works without even trying the API key. So that's what I tried to show you. That's what I was telling you about background jobs. And I hope that this kind of explained it even further why they are so useful because they can really be long running tasks and it's such a good experience for your users to instantly get a success message like hey okay we are taking care of that go ahead and relax do whatever you want and that will be extremely useful if not fundamental to this project which is workflows right we are based on these background jobs. So I think that now we have a very good understanding of background jobs and we even tested them with another fundamental of this project, which is AI, right? So we set up AI SDK and we used AI SDK within just now let's open a pull request here. So I'm going to click here, create new branch, 07 AI providers. I'm going to go inside of source control here. I will expand my screen a bit so you can see with me. I will close the graph here. Not too many unsaved files, I mean unstaged files. So I will stage all of them and I will add a commit 07 AI providers. Let's click commit and let's publish the branch. And once the branch has been published we can go ahead and simply start a pull request and let's quickly review our changes and go to the next chapter. And here we have the summary by CodeRabbit. We added a test AI button to trigger an AI job and it shows the AI job queued toast on success. We introduced a new public API mutation to queue an AI execution because we added base procedure there. And the backend now processes AI tasks using multiple providers and returns aggregated results. This is referring to the ingest function. And of course, we added AI SDK dependencies to support this new AI job execution. As always, a file by file and a cohort walk through here. But what I'm interested in is, of course, the sequence diagram. So let's just quickly take a look, even though it's fairly simple and we've already established this a few times, the user clicks on test AI. We call trpc testai.mutate. We send the event called execute AI to ingest and we immediately return back to the user success. Job has been queued. This is exactly what we were talking about, I believe, in the previous chapter. We achieved this again this time with a real world example and not just some sleep steps. And then what happens in the execute function is just calling various SDKs, three different external services. All of them could fail, all of them could take a while, and that's why it's perfect for a background job. So all of them can then run and then return back the results. But we're going to improve that even further by adding real time to it. So then users we'll see exactly which node is executing, which one succeeded, and which one has failed. And as per the code comments, it here, it told us to use a fully qualified anthropic clause on it. I'm not familiar with the models here, so I don't know. It says Gemini Flash is okay and GPT-4 is correct, but for this one that we should use version ID, I don't know. Sure, we can change it to that. I'm not too familiar with the models. and in here of course it noticed that we changed it to base procedure and that we should add authentication there so you can see how familiar it is with our protected procedure it's really like a partner to check all of your code obviously we did this on purpose because we didn't really have a proper outflow setup but yeah we are going to remove the test AI route anyway amazing job a very good pull request here as always very useful review by CodeRabbit and once you merge that make sure that you go back here select main and simply synchronize your changes this way you are up to date and then instead of your source control here just make sure to look at the graph and just confirm that you have number seven here and that you've merged it in here this way you know that you're up to date. Perfect. I believe that marks the end of this chapter. Let me just double check. Yes. Amazing, amazing job. And see you in the next one. In this chapter, we're going to focus on creating production grade error tracking within our project. In order to do that, let's first try and demonstrate an error scenario within our app. I'm going to go inside of my underscore app folder where I have a couple of procedures, my app router where I have the test AI route. And inside of here, let's go ahead and do throw new trpc error from trpc server. Let's pass in the code bad request and let's pass in a message which says something went wrong. Now let's go ahead and let's do npm run dev or if we already have it, you can just restart your next server. And now let's go ahead into localhost 3000. So now if you go ahead and click test AI, you don't even know that something went wrong unless you take a look in the terminal. And in here you can see that something is happening. A way that you can make the user aware that something went wrong is by going inside of page where you execute test AI and add the on error here and do toast error, something went wrong. If you try now, you will get the message down here, something went wrong. This is an extremely basic scenario and in here we are actually forcing an error. But errors like this are bound to happen in every application, but even more likely in an application that is so heavily dependent on user input. Because as a developer, you are very familiar with your application and you're very familiar with your code, which means that errors like this and bugs like this will not escape you. You will catch them and you will fix them. but will you fix all bugs that hundreds or thousands of your users will introduce and find out chances are you're not going to do that so easily now there are ways you can add your own logging here for example every time you throw an error you can go ahead and call some kind of logger and pass in the message here. And then that logger would store that somewhere so you can take a look. But you can see how tedious that becomes very quickly. Just imagine having to do that for all of our background functions, for all of our third-party services. Very soon it becomes unmaintainable. For that reason, we're going to integrate Sentry, production-grade tracking, which works almost magically But besides that we also going to use Sentry to integrate something called session replace which is basically a way to see exactly what your user was doing in an anonymized way to see where they clicked on the UI that started the error Because sometimes just seeing the error on your backend doesn't make it instantly clear why it happened. Even if you look at the input that was sent, it's just not clear. so when you combine that with a session replay which is able to recreate how the UI looked at the time of the error happening gives you a whole another perspective and Sentry is one of the rare error tracking tools that is able to do that but besides that we're also going to have something very interesting called AI monitoring and with just a few configuration steps you will be able to track every single AI call within your application. Not only whether it succeeds or fails, but also how long did it take and how much did it cost. And by having that information, you will be able to improve your app constantly by optimizing to cost less and last less. Let's go ahead and do that. Using the link on the screen, you can visit Sentry.io and you can create an account. I've teamed up with Sentry and they are currently offering three months of Sentry team for free. So use the link on the screen and create an account. And once you have done that, go ahead and sign in. I've logged in and this is how it looks in an empty dashboard. with no projects. If you have a project, that's perfectly fine. We're going to create a new one for this project. So I'm going to click right here on help, and then I'm going to click on documentation. And in here, I'm going to find an installation wizard for Next.js. So let's go ahead and run this command right here. And just for information purposes, I'm also going to show you the exact version of this installation let me go ahead and paste this and just try doing version first this way if you want to use the same version as me you can do it and now I'm going to run the actual command so I recommend shutting your app down for this one and just running the command so in here it tells us that we have uncommitted or untracked files in your repo. So it is basically warning us that Sentry Wizard is going to install some files. So be careful, you know, if it overwrites them. We don't really care about this. We just added some errors and some toasters for showing that errors. We can even unstage all of those. It really wouldn't matter. So it's completely fine. So I'm just going to select yes. Now it's asking us if we are self-hosting Sentry. That's right. You can self-host Sentry if you want to. that's another reason why I highly recommend using Sentry for error tracking. If you don't want it, you don't have to, but you can still self-host it. So let's go ahead and select Sentry SaaS, because we just created an account on their website. Do you already have an Sentry account? Yes, we can now select yes, and this will now open the page here. So select your organization, and let's go ahead and create a new project. I'm going to call this NodeBase, and let's go ahead and click continue. And now the wizard will connect so we can return to the terminal here. And here it is installing Sentry Next.js with npm, our package manager. For this prompt, do you want to route Sentry requests in the browser through Next.js server to avoid ad lockers? You can see what will happen if you select yes. It can potentially increase the server load and hosting bill. So depending on what you want, you can select no, but then browser errors and events might be blocked by ad blockers before being sent to Sentry. So you kind of have to decide for yourself. For testing purposes, for demo, it's completely fine to select yes, we are just doing this locally. So let's see Sentry in action as what it can do in full, right? Do you want to enable tracing? Yes. Do you want to enable session replay to get a video-like reproduction of errors during a user session. This is extremely useful and one of the most impressive things I've seen. Make sure to select yes for this. Do you want to enable logs? Again, this is also very, very useful. You will be able to follow your application logs from start to finish. So select yes. And select yes for creating an example page so we can test if the sentry setup was working. And in here it gives us a warning that we are using TurboPack. It is warning us that Sentry is only compatible with TurboPack on Next.js version 15.3.0 or later. So we are good. If you're using TurboPack with an older Next.js version, just remove dash dash turbo or dash dash turbo back from your development command so we don't really have to do anything here are you using a ci cd tool uh we can select yes for this and now let's go ahead and copy the century alf token and let's go inside of dot environment and let's add century and just paste it here. I like to put it inside of quotes because of syntax highlighting. And let's say yes, continue. Optionally add a project scope and NCP server. So I'm not working with NCPs. So for me, this can be no. And as you can see, you can add it later anytime. Here it is successfully installed Sentry Next.js SDK. So we can now npm run div and we can go to forward slash Sentry example page. Don't forget to remove Turbo if you are on older Next.js version. Not the case for us. So let's do npm run dev all. So we start both ingest and Next.js. Let's go to localhost 3000 and forward slash sentry example page. And now in here, let's go ahead and throw a sample error. And if you get a message error sent to sentry, it means everything is good. So let me go ahead and refresh here. In my Sentry, you can see how now I got both the front-end error and the example API error. So that's how Sentry works. The error that just happened wasn't explicitly logged. Sentry will simply catch every single log inside. Sentry will catch every single error within our app. That's exactly what we want. We don't want to concern ourselves with whether we forgot to track an error. Sentry will monitor every single error that's happening within our app. So if you scroll down here, make sure you select the front-end error. You can actually see the session replay from this anonymous user. And you can see how it managed to recreate what was happening in the UI at the time. and you can see the user clicking on the button. You can see the success messages. You can see the arrow moving. So this was all done using metadata and telemetry that's available to recreate the scene. And this will help you a lot in production when errors keep happening and you don't know why. Just go to the front end and see the session replay. And this will give you a clearer picture of why it happened. And you can actually follow the breadcrumbs about how this happened, right? So you can see that right here, this sentry example error is being thrown. This error is raised on the front end of the example page. But you can see what actually happened, right? We fetched API sentry example API, and in here we got 500 error. So that's why the exception was thrown. So the more you explore sentry, the more you're going to understand how useful this is. These are the logs that I have been talking about. So you can see exactly everything that was happening. We navigated to a sentry example page. And then in here, we did a UI click. And you can see exactly which button we clicked, exactly what was the HTML element within the button, which was a span. And then you can see exactly what that button triggered, what we received from that button, And finally, the exception that was thrown on the front end. And using all of that telemetry, we are able to achieve something like this. This will help you greatly within your projects. And if you use the link that I showed on the screen, you can actually get Sentry theme three months for free. But even after the free trial, if you don't want, if you don't have the budget, Sentry is still free and has an extremely generous free tier. I highly suggest just keeping it here in case some errors happen. And let's go ahead and check out things even further here. So we also have sentry example API error here. In here, we can see some other things. In here, we can see the backend version, right? So you can see how different this is now. In here, we are tracking internal files of Next.js and where it happened and how it happened. And again, you can follow the breadcrumbs to understand why this happened. You can see how it has the trace preview specifically created for Next.js. So you understand exactly every single thing that is happening here. Page load, century example page. Then HTTP server. you can see in how much detail you can track these errors. This is what production grade error tracking looks like. But we are barely scratching the surface of what Sentry can do. So while this is super cool, I can talk about this for days probably. So I'm going to stop myself here. And now what I want to do is I want to go ahead and just show you a few more things besides the issues here which are basically your unresolved issues and yes from here you can actually assign your members to these issues You can mark them as resolved. You can resolve them in a commit. You can go ahead and connect this with GitHub and create a pull request, a GitHub issue. There is so many things you can do here. There are also different views here under Explore. You can see individual traces or you can see the logs in here. So let's go ahead now and let's focus on one specific thing, which is insights. And in here you can see AI. So let's go ahead and configure AI agents because this is another super cool and useful thing that we can do. so we already installed sentry SDK and we are using Vercel AI SDK so this is good sentry next JS let's just double check I think that when we run the wizard we I'm pretty sure we got all the packages so inside of here let me just check whoops sentry next JS here it is all right no need to do anything here. Instead, let's just focus on next. And that is configuration. So add a Vercel AI integration to your Sentry.init call. So I'm going to copy the integration block right here. And then I'm going to find my server config as per their documentation. So we have the edge config and we have the server config so after dsn here i'm going to add integrations add diverse cell ai sdk integration to sentry.server.config.ts it's added right here record inputs set to true and record outputs set to true as well and let's just quickly verify so tracing must be enabled for monitoring to work. Tracing sample rate is 1.0. And let's just see whether we have that. Traces sample rate is set to 1. I believe that's the same as 1.0. Great. And we can also do send default PII simply because they also have it here. So I will set it to true. And now in order for this to work, we have to find where we use generate text and we have to add the experimental telemetry and execute it. So let's go inside of our functions.ts, inside of ingest, where we use the generate text. So after the prompt here, let's also add experimental telemetry. Now, depending on when you're watching, this might have become telemetry, because obviously, since it has the experimental prefix, it's bound to happen at some point that it will just be called telemetry. So if experimental is not working for you it's obvious that the API has changed and I'm pretty sure the name will stay telemetry. So let's go ahead and add this to all the places where we are using generate text. So these three places right here and let's click save. And once we've done that let's go ahead and click on next and now this is waiting for the project's first agent events so i'm going to go to localhost 3000 and refresh here and i'm going to click test ai and now i'm going to just watch here and see if it will detect it so as far as i remember we first wait for a few seconds and then we fire the events and after a few seconds go ahead and refresh if it doesn't refresh for you and you should be able to see this screen and in here you can see traces models and even tools right now we are not using any tools so we just have to focus on these two and you can already see the number of llm calls for GPT-4, for Gemini 2.5 Flash, and for Cloud Sonnet 4.5. You can see exact amount of tokens that we've used. You can see GPT-4 is using significantly more tokens than Gemini. So perhaps if you want to make sure your app is the cheapest one amongst competitors, you can maybe default your users to use Gemini 2.5 unless they select otherwise. this way they will have the cheapest amount of token spend so this is what i was telling you about how you can learn more about your application if you add sentry you can make smarter decisions this way inside of the models tab you can see this in another graphical example you can also see errors which are happening for example my cloud keeps getting errors right why is it getting errors well it's getting errors because I have no API key inside and what they've done is they've reused their amazing traces and everything that you usually use for normal errors and they have added it to Vercel AI SDK so now we can see exactly what went wrong in this type of environment as well and this is extremely useful because AI models are very hard to keep track of right and now you have them in one place you can filter by so many things by your projects by production by development in the last hour last 24 hours and you can combine it with everything that we have learned in the issues tab so you can follow the trace from the front end all the way to the back basically. Sentry is an absolutely crucial part of this project, but you will only see its power when you go to production. And hopefully all of you will go to production here. So you can find the AI again here in AI New. You can go ahead and play with the models. You can play with tools here. And the more you use your app, the more errors you're going to get here, the more things will happen and this will be another way of fixing what you missed so we will definitely come back to sentry at some point and i just want to see whether we have logs here so let's see we already installed this let's go ahead and click next uh so now we also need to add sentry console logging integration here and what will happen now is remember when i told you that let me just see so we are adding this looks like if they don't define where i think we need to add it to both so let's add it to server config here so integrations after century versell integration also add century console logging integrations and track log warn and error here and let's do the same in century.edge.config here so i'm just going to add integrations here and add it like so. So now basically in your functions here, if you just add some logs, you are also going to track things. So if you add console log or maybe console warn, something is missing or console.error, this is an error I want to track. You no longer have to develop your own logger abstraction and send that somewhere because after you've added this integration sentry will now do it for you so let's go ahead and click next here uh and let's see okay perhaps we have to use this logger my apologies uh perhaps i understood it wrong but i'm pretty sure both are true i think we're just using this to forcefully test it i think both will work so i'm going to directly try from their logger Either way, you don't have to create your own, right? So both will now work. Let's see. User triggered the test log. And let's go ahead inside of our app here. Let's refresh and let's click on test AI button. And now in here, let's go ahead and wait a few seconds. And then let's try and refresh to see if this will now work. so it takes a while but after a few refreshes you will start to see the logs here as well and from now on whenever you want to log something and make sure that it is recorded you can use the sentry integration to do so as well so as i said we will definitely be visiting this dashboard throughout the tutorial so we can keep track of our tokens used our llm calls and all the errors that we might have missed that are happening in the background. Because you can see, I don't always have this open, but this could be full of errors as far as I know. That's why we're going to make sure Sentry is keeping track of that and we no longer have to depend on ourselves on thinking about every single possible errors that both us and our users can do. So I hope I kind of showed you the power of Sentry and why I like it so much. and let's go ahead and see if that's all we planned for this chapter i believe it is we've set up sentry we've shown session replays logs and ai monitoring and we will continue to demonstrate throughout the rest of the project and now let's just push this to github so since this was mostly just installation wizard we don't really have to review this pull request so let's go ahead and just create a new branch 08 error tracking like so let's go ahead here in the source control and let's go ahead and stage all changes and let's do 08 error tracking and let's hit commit and let's hit publish branch i also just want to tell you that if for whatever reason this chapter was unsuccessful for you in a sense that something is not working do not worry so all of this is just a improvement it is not required in a sense that you will not be able to finish the project without this i just think it's an extremely useful thing to have especially for a production grade application so here i am opening the pull request and as i said This was mostly just an installation wizard and some testing scenes. So we can immediately merge this pull request\nWe are just going to keep it as a separate branch, right? But we don't really have to go through the review. And once you have merged this, you can go back inside of your project here and make sure that you go back inside of your main branch. And as always, make sure that you synchronize changes so everything is pulled from the branch that we just merged. And I believe that marks the end of this chapter. Amazing, amazing job. You can also go inside of your source control here, graph to confirm that you have detached and merged as always, and see you in the next chapter. In this chapter, we're going to create the layout for our dashboard part of the application. So let's go ahead and start by improving our file structure. And then let's add the layout files, which are going to render the sidebar, which we going to use as our main navigation towards many various routes. So let's go ahead first and see if our apps are running. So just a quick reminder, I am using npm run dev all, which uses mprox and the config file to simultaneously run ingest and next. If for whatever reason that's not working for you, you can always individually run npm run dev and npx in just CLI latest and dev. That's the command. Even though for this exact chapter, we only actually need this. So I will do this simply because I prefer it this way. And let's go ahead and go to localhost. and now let's go ahead and improve the structure of our files here and sometimes you might see changes like this happen it's my amp-prox now let's go ahead and go inside of the source app and in here let's modify this a bit besides alf i'm also going to have dashboard like this another route group so this will not be a part of the URL. It's just going to be used for structure. And now inside of here, let's create another folder, one called editor, and the other rest, as in the rest of the routes. We can think of a better organizational name later. The reason I'm separating editor is because it's going to have a very specific layout. So that's why I'm having a separate organizational folder for it. And I'm going to keep well the rest of the routes which are exactly the same and share the same layout in the rest folder so let's go ahead and start by adding some things here so let's add workflows which will be one of our entities and let's add page.tsx let's do const page return paragraph workflow page and let's do export default page and we can actually keep this simpler workflows as simple as that and now that we have that ready let's go ahead and let's remove page.tsx entirely. So remove it so that this breaks. And when you do that, you can also remove logout.tsx. We're not going to need that either. So inside of your app folder, you should now have global error, which was added by Sentry, and you should have layout file. Let's focus on the dashboard for now. Inside of REST, we now have workflows and page.tsx. and that is actually going to be our main route. So in order to make that the default route, let's head to next.config.ts and inside of next.config, let's go ahead and do an asynchronous redirects function and in here, let's return an array. Inside of this array, I'm going to open an object with a source of forward slash as in the root page and change the destination to be workflows and set the permanent to false. The reason I'm telling you to set this to false is if you have any other Next.js projects on your machine, setting permanent to true will make them redirect the root page to workflows as well. So for that reason, it's better to put this to false. It will work exactly the same and it will help you in development mode. After you do that, go ahead and restart Next.js. So use the letter R to restart it and refresh localhost 3000. And you can see how that is immediately redirecting me to forward slash workflows. So I am now immediately redirected to here in case you can't see. So if I try and delete the workflows route, again, I'm redirected back here. That is exactly the behavior we intended to have. Now let's continue developing here. Now inside of my app folder, dashboard, rest, I'm going to copy workflows and I'm going to make this credentials. You can select yes if it asks you to update the imports for credentials. It is basically going to modify the .next cache file. It looks kind of buggy, but you don't have to worry too much about this file. You can see it's unsaved. I can just click save and close it, and I can just collapse the .next folder. It really doesn't matter because it will be rebuilt each time we restart the server. so inside of credentials page change these two credentials and now let's copy that and paste it here and change this to executions and same thing for imports update here it's a little bit buggy you can just go inside of the unsaved file save it close it and collapse the dot next folder so you don't accidentally develop anything in there. Go inside of the executions and change the paragraph to executions. So now you should have three routes, credentials, executions, and workflows, with workflows being your default route. So now that we have that, let's create individual routes here. So for example, inside of here, let's add credential ID like so. And inside page.tsx, I'm going to copy the existing page content, paste it here, and this will be credential ID. And in order to make this fun, let's create an interface, page props, params, promise, and inside of here add credential ID to be a type of string. Now in here you can assign the page props. Whoops. You can extract the params. Make this an asynchronous method and simply destructure credential ID from await params. and then go ahead and render the credential ID. So be careful about two things. First, make sure you develop this folder inside of the credentials folder. Second, make sure that you've added the square brackets. This makes it dynamic. And third, make sure you use the proper capitalization. Credential is spelled in lowercase and id is spelled with a capital i so credential id like this if you named this credential id too for example then inside of the page props this will no longer work it has to be credential id here as well that's why i'm telling you that capitalization is important so make sure that you have named this credential id with a capital letter i it's a little bit problematic here because because of this font they look very similar and again this validator is showing up in the types you can just save it overwrite it it really doesn't matter you can even delete the entire dot next folder and just restart the server and it will fix itself don't worry about it So how do you test if you did this correctly? Well, go ahead and go to HTTP localhost 3000 credentials 123. So I'm going to copy this route and I'm going to change the URL to that. And there we go. Credential ID is 123. And if you're wondering why does this need to be a promise? it's simply the way Next.js params API works. So if you remove asynchronous and just do this and if you didn't type it as a promise I think it will still work. Yeah, you can see it still works but you also get an error. This route used params.credentialID params should be awaited before using the properties. Learn more right here. dynamic APIs. So let me paste this here and here it is. So dynamic APIs are asynchronous. You can see that if you do this you get that warning. So because of that you have to make it asynchronous and you have to await the params. Exactly what we were doing before so I'm going to revert it to that. Perfect. So that's how we're going to handle individual credential. Let's copy this route and let's paste it inside of executions. And let's change it from credential ID to execution ID. Again, you can update the import, save the validator, close it, and collapse the .next folder. It a little bit annoying Let go inside of execution ID page and change this from credential ID to execution ID and change this to be execution ID So now the same thing, HTTP, localhost 3000, executions, one, two, three. So I'm going to change the URL to that now, paste it, and here we have it, execution ID. so if you accidentally misspell this you will not be able to see your id and that's how you know that you named something incorrectly it could be a typo in here or it could be a typo in here or maybe it could be the invalid structure and for the workflow id we're going to do it a little bit differently so that's why i kept all of this in the rest folder because they all share the same layout and all of this is fine but workflows will only be rendered inside of rest here but very specific workflow id will be rendered in the editor route group yes you can do that you can create workflows as a folder again here but you cannot create a page.tsx inside of here again that would be a conflict. But what you can do is just create its child route, workflow ID, like this. Now both routes will work. Workflows will be using the REST organizational folder. And if we add a layout file here later, which we will do, it will share the REST layout. Whereas the workflow ID will be using the editor layout. That's why we are doing that separation. Inside of workflow ID, let's create page.tsx and let's copy it from one of the existing ID pages. And let me just remove this. We no longer need it. Copy it, paste it in the new workflow ID and change this three to be workflow ID and change this to be workflow ID. Once you've saved that file, you can very easily just go to the root of your localhost because you will get redirected to workflows and then simply add forward slash one two three and you will see workflow id one two three is working so this is what you should go to http ignore the fact that i copied https it's the browser's mistake it's HTTP. Perfect. So now we have improved our file structure. And now that we have that, we are ready to actually build the proper layout for this. So we're going to go inside of dashboard here and create a new file layout.tsx. This layout will be shared across both the editor and the rest. So let's go inside of layout to fix this error. Const layout and let's return here sidebar provider from components UI sidebar which we added when we initialized chat CNUI. So you should have the sidebar provider inside of your components UI sidebar. It uses the use is mobile that's how you can recognize it i didn't just randomly say that fact and now let's just quickly give the props here children react.react node children and in here let's add sidebar inset from the same folder so sidebar inset and render the children inside and give this a class name background accent with a 20% opacity. And now let's go ahead and let's add app sidebar here, which is a self-closing tag and we don't yet have it. We have to develop it. So I'm going to close everything, go inside of source components and create app-sidebar.tsx. I'm not going to create it inside of the UI folder. I like to reserve the UI folder for chatCN components. My own are going to go here in the components folder. So inside of here, let's mark it as use client because it will often be rendered in server components. So from here, let's go ahead and import all the icons that you are going to need. Maybe we are not going to use all of them immediately, but let's just import all of these from Lucid React. let's import image from next image let's import link from next link let's import use path name and use a router from next navigation and now let's import everything we need from the component from the sidebar component. The sidebar itself, content, footer, group, group content, header, menu, menu button, and menu item. Perfect. And now let's go ahead and let's define the menu items here. The menu items is going to be an array of objects. Each object will be a definition of a For example, we're going to have workflows and the items are going to be an array of routes. So the first one will be workflows. Icon will be folder open. And the URL will be workflows. And I just want to change something. since all of them are using the icon version. I want to use the icon version for this as well. You can do both in Lucid React. So for each credit card, there is a credit card icon. So you can see if I remove this, I don't get an error because both exist as exports. But I really prefer having the icon at the end because if you import link, you can see it's a duplicate identifier. But if you have a practice of importing things as link icon, it doesn't happen so that's why I prefer being consistent here so I'm going to change this to this and let's stop here so that we can actually develop the app sidebar and see the results export const app sidebar like this let's go ahead and return sidebar collapsible is going to be a type of icon let's add sidebar content and inside of here menu items dot map get an individual group and for each group let's go ahead and let's return a sidebar group element give the key a group dot title and inside of here, let's add sidebar group content. Inside group.items.map, get the individual item, sidebar menu item, with a key of item.title. Inside of here, use the sidebar menu button to finally render the element. The sidebar menu button will have a couple of attributes. For example, toolpip, item.title. Is active, which for now we can hardcode to false. We're going to make it dynamic later. Make it as child. As child prop will make sure that this element here becomes whatever we put as the first child inside. Thus the name as child. This is useful for when we want the sidebar menu button to be a link because it is not valid to render an href element within a button. So because of that, this button will become the link element. And let's give it a class name gap x of 4, height of 10, and px of 4. Now inside of here, let's use the link element. Let's give it an href of item.url. Inside, let's render item.icon, which can be whatever we have defined here. For example, folder open icon. It's a self-closing tag and it's just going to have a size for class name. And finally, a span rendering the item title. in order to make this link faster we are going to prefetch like so in production this will make sure to prefetch the content of this href on page load so when the user clicks on it it will almost be as if it is as if it as if it was already loaded which it is it will act like it is cached so now that we have that we are ready to render it and see something so i'm going to go inside of my layout in app dashboard and let's import app sidebar and let's go ahead all the way here and maybe zoom out a bit make sure you zoom out otherwise you will not be able to see it but here it is the workflow so let me zoom out as much as i can because if i zoom out too much it collapses to mobile mode. So if I click workflows, you can see it redirects me back to workflows. Perfect. Now let's focus on creating the rest of the menu items here. So besides workflows, we're going to have credentials. So let's add credentials here. And let's also have executions here. Like that. So all of them inside of items and the title here can really be home because or maybe main like that like main menu items and now on desktop mode here we should have workflows we should have credentials and we should have executions and all the routes should work because we just developed the structure for them now let's go ahead and make the app sidebar look a little bit better by adding the sidebar header let's add sidebar menu item let's add sidebar menu button let's go ahead and give the sidebar menu button as child again and let give it a class name gap x4 height 10 and px of 4 let add a link in here and let prefetch to forward slash workflows whoops forward slash workflows okay I think something wrong link like this href forward slash workflows all right and prefetch and i think that's good let's add an image with a source of logos forward slash logo svg we should have already been using this you can see in alf layout so whatever you used for the logo in alf layout is the same you should use here the alt will be node base the name of our app width will be 30 height will be 30 as well you can find your logo in the public folder here logos logo.svg perfect and below the image add a span with a text of node base the name of our app with a class name font semi-bold and text small let's go ahead and see how this looks like already looking much better And you can see that this acts as the home button. Perfect. In fact, you can make the Ahrefs go to forward slash. This way, it's a true root page. And just accidentally, we also redirect to workflows. So nothing really changes. But I think this is more semantically correct, that this leads to a root page and this leads to workflows. Those are the same because we redirect, but I think you get the idea what I'm trying to explain here. Perfect. Now that we have that, let's go ahead and make sure that we can highlight what is the active route. Const router use router. Const path name use path name. And now let's go ahead down here in the menu items. And in the sidebar menu button for the is active, let's check if item.url is forward slash. In that case, let's check if path name is also forward slash. Otherwise, let's check if path name starts with item.url. So now once you've added this, you will see that each of these will be properly highlighted and selected. The reason that we need to do this little trick is because if you don't do it, you can get into a situation where all routes are considered active. So you have to make an exclusion for this specific route page, even though this can't really happen with our app because we redirect to workflows. But if you ever want to change it, it will happen to you. So leave it like this. It's better. Great. Now that we have that, let me just go ahead and see one thing I don't like is that inside of the sidebar group content none of these have any space between each other maybe I will focus on that later I want to go ahead and create the sidebar footer so outside sidebar content create sidebar footer sidebar menu and inside of here let's add sidebar menu item sidebar menu button let's go ahead and add a tooltip here upgrade to pro class name will be the same as we've had so far for our buttons gap x4 height 10 and px of 4 on click for now will just be an empty arrow function and inside we are going to render a star icon and a text upgrade to pro and then let's go ahead and copy the sidebar menu item here paste it below and change the tooltip here to be billing portal let's actually do it like this change the text to billing portal use the credit card icon and I think everything else can stay the same and there is one more thing missing here so you can see now we have upgraded to pro and we have billing portal and if you want to get rid of this next JS indicator here, you can do that by going inside of the config and turn the dev indicators to false. And I would suggest restarting your next route by pressing the R letter, which will reset it like this. And then restart your app. So what we have to do now is we have to do the sign out button. So let's copy this sidebar menu item entirely. Paste it here. Set the icon to be logout icon. Change the text below to be sign out. And change the tooltip to be sign out as well. And that will be the finished look of our sidebar. So credentials, executions. yeah you can see how they are not so mashed up down here but they are mashed up up here so i was definitely right that something is off but okay we will definitely resolve that let's focus on actually creating the sign the sign out method so this is actually quite easy all we have to do is we have to import auth client from lib auth client and let's go down here to sign out and let's call auth client dot sign out open an object fetch options on success router.push forward slash login just like this and now when you go here and click on sign out you should get redirected to the login page let me try again sign out not working too well and I think the fault might be okay now it's not working because I am already signed out so yes we can't really properly demonstrate this because none of our pages are redirecting at the moment so let's do the following first I'm just going to debug a little what's going on here so we have sidebar group we have sidebar group content we have sidebar menu item here sidebar menu button we have as child we have link yes not exactly sure why this is happening I think that I'm missing inside of sidebar group content we need sidebar menu yes and just wrap the iteration around the sidebar menu and indent it here and I think that that will fix it here we go that looks much better much more visually cleaner and now let's go ahead and properly protect our routes to end this chapter. So let's start by going into workflows page.tsx right here in the rest. Let's go ahead and make this asynchronous and very simply let's await require out. I already explained why I prefer doing this individually in each page rather than using a middleware. I personally consider this and the middleware just a helper for user experience. The real authentication and security layer is in our data access layer, our trpc-protected route. Everything else here is so that the user doesn't see errors or weird flows like we just had, right? So that's why I don't want to teach to use the middleware because people start to use it to protect their API routes. And I personally protect individual pages like this. You can see exactly what's going on. Executions, let's do the same thing. So we add this. We turn this into an asynchronous page. And we import. And I think I can copy this and just paste it in credentials too. Perfect. And then let's do the same for individual. credential ID. Same thing. In here it's even easier. We can just await require out from lib out utils. Let's go ahead inside of execution ID and do the same thing. Require out from lib out utils and there is one place left and that is workflow ID which is located in the editor here. So let's make sure to add that here to require alph from lib alphutils. And I think that's all of our pages protected. And now we should be able to demonstrate the logout in a better way. So 1, 2, 3, 4, 5, 6, 7, 8 is my password here. I'm logging in. I am immediately redirected to workflows workflows is highlighted i can visit credentials i can visit executions and if i click sign out there we go i am redirected back to login amazing so i believe this puts us in a much better position as opposed to previous state where we had to you know put a bunch of things in page.tsx and it was starting to get a little cluttered. So the next steps here, let me just fix one, two, three, four, five, six, seven, eight, log in. The next steps here would be to create a add new button in the workflow and to create a header which we are going to use to enable this on mobile mode and actually want to use this chapter to add the header as well simply because it is very very easy to do so so we're going to go inside of the rest folder app folder dashboard rest and in here create a layout dot dsx. We can go ahead and copy the existing layout here and just paste it, but remove the sidebar provider, remove all of these. Instead, you can just go ahead and add a fragment like so. You can wrap the children around a main element, give it a class name of flex1 and instead of adding anything here, let's just do app header. And now let's quickly develop app header which should be super simple inside of source components let's develop app dash header dot tsx and let's paste it here so import i mean paste it i pasted it you build it right so import sidebar trigger from components ui sidebar export const app header we are using the simple header element and we give it a class name of flex height 14 shrink zero items center gap two border bottom px4 and bg of background and in here we render the sidebar trigger so now that we have the app header here make sure that you build it again i pasted it from my source code we didn't copy it from anywhere uh it was my mistake and now let's just import the app header from here. So very simple header here. And you can see already how it looks. So now on mobile, you should be able to open the sidebar header. I mean the sidebar. Okay, perfect. So I think that's more than enough for this chapter. Very clean, very smooth. We protected our routes. We created a very nice layout. And yes, so now the rest folder has this layout where it has the app header. but the editor will have a very specific header of its own. That's why I didn't put it inside of here. That's why we are separating it. And if you're wondering about a better name for this folder, maybe home will be better or main. I don't know if you really dislike Rust because I don't like it too much, but it's the first thing that came to mind. Great. I think this is more than enough for this chapter. So again, I have 14 files. Maybe you have 13 if you don't have the mprox.log. So it's when an error happens, you get a new log file. So yeah, don't worry about that. All right. Now let's go ahead and see if that's all we intended to do. I'm pretty sure it was. We improved the file structure. We created placeholder routes. We created the sidebar layout. Perfect. So 09 sidebar layout, I'm going to create a new branch, 09 sidebar layout and then I'm going to push that branch. So I'm going to stage all of my changes, 09 sidebar layout, I'm going to commit those changes and I'm going to publish the branch. and once I publish the branch, I'm going to go ahead and I'm going to create a pull request as usual. Let's go ahead and review the changes and let's wrap up the chapter. And here we have the summary. New features. We added new dashboard layout with header and collapsible sidebar navigation. We dedicated pages for workflow credentials, executions, including detailed views by ID. Root URL now redirects to forward slash workflows. All dashboard pages enforce authentication. We did some refactoring when it comes to navigation. We moved sign out around. It basically refers to the fact that it was all in one page.tsx and then now it is in the sidebar. As always, a walkthrough file by file here. And for the first time, I don't think we really need to look at the diagram simply because there isn't really any business logic involved here. We know exactly what's going on, right? We already went over all of these outflows in the previous chapters, so we don't have to focus on it too much. And we were pretty good. No actionable comments beside the typo that I did. Instead of upgrade, I spelled up gate or I don't know. I think I missed something. Oh, the tooltip is misspelled. All right. Great. So I believe that marks the end of this chapter. I'm going to merge this pull request. You merge it as well. And after you did so, go ahead and change back to the main branch and go ahead and synchronize your changes. Click OK. And let me just go ahead and review everything. I believe that marks the end. It sure does. amazing amazing job and see you in the next chapter in this chapter we're going to integrate payments and subscriptions to our application and we're going to do that using polar an open source payment processor so why am i choosing polar over all other methods looking at their website You can find exactly what Polar is and how it works. Polar is an open-source billing infrastructure platform designed specifically for developers who want to monetize their software without the complexity of traditional payment systems. Polar is also a merchant of record, which basically means that you can leave the billing infrastructure and international tax headaches to them and just focus on growing your business instead. They handle VAT, GST and sales tax in all jurisdictions. They also do proper EU VAT handling and they also do automatic tax calculation for you. This is not everyone's cup of tea. Some people prefer using Stripe and when they met the threshold for tax they have their accountant handle that. That's a perfectly fine decision to do so. In my case I have a company registered within EU and it is very important for me to have a merchant of record. But if the first two reasons aren't enough for you, there is also a third reason, developer experience. I am not exaggerating when I say that for the time it takes me to integrate Stripe in just one project, I can integrate Polar in 50 projects. And I am not exaggerating. You're going to see that in a second when we actually start developing here. So Polar was developed with developer ergonomics in mind. They put developer experience in the front seat and they're not lying when they say that. You will see how incredibly easy it is to integrate Polar within any adapter that you need, be that Next.js, TypeScript, or something even more specific like BetterAlt, which is our use case. And on top of all of that, Polar is completely open source, which I've already mentioned in the beginning. And if you're still having doubt about their reliability, given that they are a new player in this payment processing world, I can tell you that I have personally been using Polar over half a year for all of my sales, and I have not once had a bad experience. And this is also true for their support team who is extremely responsive and here for any questions that you might have and same goes for the amazing developer community that they have created within their discord and here's some other information polar is the cheapest merchant of record on the market and no hidden fees which is unfortunately a practice for a lot of merchant of record providers and I'm very happy that Polar does not do that. You can very clearly see exactly how much money goes into your bank account after all the tax has been handled. So now let's go ahead and use the link you can see on the screen to actually visit their landing page here. So you can turn your software into a business with six lines of code and they are not joking when they say that And you automatically get subscriptions, benefits, checkout links, usage billing, customer portal, metrics, and even more than that. When they say integrate under a minute, again, they are not joking. The only reason this chapter is not one minute long is because I'm taking time to explain what I'm doing. So in here, they've listed some of the more popular adapters like Next.js adapter. And you can see how easy it is to add a checkout route here. but they also have some very specific adapters like better out which is absolutely amazing because that's exactly what we need and in here you can already see something interesting inside of the better out config they have this field called create customer on sign up set to true this one line is so useful that it will save us so much headache and so much time from making sure that we can easily check whether any user in our database is a pro tier or a free tier customer. Let's go ahead and just try and integrate Polar so you can see exactly what I'm talking about. So go ahead and log in. I'm going to be using Google here. Now depending on your state here you will either have a project or you will have a new organization screen which you can access manually by going on new organization here. Now we're going to have to create two organizations here. So I'm going to call this first one node base and I will call it production. And organization slug needs to be unique. So I would suggest adding some unique identifier here. I will add CWA as in code with Antonio. So this will be taken for you. So make sure you add something else. In here you have supported use cases and prohibited use cases if you're interested for that\nfor your own custom app later. Make sure you understand those terms and click create. And after you have created an organization, you will be prompted to create your first product. I'm going to call this NodeBase Pro and the subscription will be unlock the full benefits of NodeBase. You can add some subscription image if you have it. Otherwise, you can go into pricing, which can be a one-time purchase monthly or yearly. And I'm going to set this to be $29.99. For the automated benefits, you can offer a Discord invite, a GitHub repository invite, file download, license keys, a lot of things. And they will automatically be revoking if the subscription has failed. And let's just click create a product. And once the product has been created in here you will be asked to integrate checkout but i would actually suggest that you just click go to dashboard so what you're looking at right now at least of me at the time of me making this video is you are in production now right so you can't exactly test this product i just wanted you to have your node-based production organization here but you don't really need to get the you can find the developer token here later when you need it in production but for development you actually need to click go to sandbox and once you are in sandbox you will see a big screen here changes you make here don't affect your live account payments are not processed and in here you have to create a new organization once more so let's go ahead and call this one node-based development and again I'm going to add an original slug here. Make sure you click I understand the restrictions and create this new organization and now you will have both your development and your production organization. So let's go ahead and call this NodeBase Pro. Unlock the full benefits of node base. Let's go ahead and set the monthly price to be $29.99 and let's click create product. And now we can actually follow the checkout instructions here. So I'm going to go ahead and select better out here and then I'm going to follow the install dependencies script. so we have to install better out which we already have so no need for that we can just focus on the other two packages here so npm install polar sh better out and polar sh sdk make sure you install those two packages within your project and then go ahead and create a new environment variable. I'm going to call this app development and it will never expire. And I will select all scopes simply so it's easy to develop with this. And let's click create. Go ahead and copy this immediately. And let's go ahead and you should now have two uncommitted changes here. So basically polar sh better out and polar sh SDK. Now let's go inside of our dot environment file here and let's add polar. So I'm going to add polar access token and I'm going to store it right here. So this is obviously for development. Great. And now that we have that looks like we also need to add the polar success URL. So let's quickly add it here. Polar success URL is just going to be our app. So I don't think, yeah, basically exactly the same as better out URL right here. Perfect. We have that ready. And now let's go ahead and integrate the checkout. So what we're going to do now is we're going to go inside of alph.ts instead of source lib where we have the prisma adapter and the email and password so let me show you how that looks like here in the file structure instead of the lib folder alph.ts and inside of here let's go ahead and let's import checkout polar and portal from polar sh better out and now let's go ahead instead of the lib folder and let's create a new file called polar.ts in here let's go ahead and import polar from polar shsdk and export const polar client new polar access token process dot environment and let's go ahead and add polar access token and make sure to add server sandbox i'm going to add a comment to do change in production so just double check that you have correctly named the polar axis token here that you don't have any typos otherwise it will not work and yes you can also create some kind of logic here for example I think in the database.ts we do that if process.environment.node environment is not production in that case use sandbox otherwise production like this and then you don't have to do it manually but you might have to modify this a little bit further if you will have staging or if you will have like some development previews right which are deployed because then it will use the production server so yeah i'm not sure if that's what you want but the simplest way is this but maybe a more reliable way would be polar server like this and then depending on where you deploy you would have to modify it so let's go ahead and just just for now let's leave it at sandbox i think it's easier so polar client sandbox let's go inside of database my apologies inside of our alf.ts here and now let's import polar client not from here but from here polar and then let's simply go ahead and add plugins in here open polar set the client to be polar client create customer on sign up set to true, use, it's going to be an array, checkout, and portal. In checkout, let's go ahead and add products. Open an array here and open an object and add product ID and add slug. Now let's find our product ID and let's choose the slug. The slug can be whatever we want. So I will set this to be pro and I think you can actually find you can see they pre-fill the product id for you so you can actually copy it from here but just in case you don't want that so I'm going to add it here now but just in case you closed this screen or cannot find it you can go to dashboard go inside of your products and click copy product ID and then you will be able to find it here. And then let's also add inside of checkout success URL process dot environment polar success URL and authenticated users only set to true. This way your customers can only be users who have an account using better elf. so in order to test if we did this correctly we already added portal all right I just wanted to make sure that's true in order to test this let's do the following we don't have to add anything new to our prisma schema instead let's do npx prisma migrate reset so let's remove all users from our database now. So all data will be lost. Yes, the reason we are doing this is because we just introduced create customer on sign up field. So I'm going to do npm run dev all I'm going to go to localhost 3000 here. And I want to to keep your polar open and click inside of customers here. You can see that I have no customers right now. But the moment I created this one, so let me go ahead and click sign up this time antoniomail.com 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 and when i click sign up nothing will change for the user really so let's just sign up here we are but on polar now when you refresh you now see your new customer here so that's what happened when you click on antoniomail.com here and when you scroll down here you can see something called external id this external id is a one-to-one mapping of what's currently stored inside of your database so if i go ahead and close this and open my users let me just refresh this so I can load the user. You can see the ID starts with capital T then lowercase d and oops I thought I was here. My apologies. You can see that is exactly the ID that's been stored here in the external ID column. So that's right. Your customers are now automatically created. You don't need a webhook. You don't need any after sign up event. All of this happens automatically and very reliably for you. And the rest is now extremely easy. So let me show you. So you're probably wondering, all right, so now when the user clicks upgrade to pro here, how do we upgrade? You will be amazed by how simple this is. Let's go instead of app-sidebar to develop that. And we don even need to add any new imports because we already have the alf client All we have to do is find the upgrade button and change the on click to call alf client dot checkout like this and select the slug to be pro but of course i missed one thing which you can immediately see by the lack of type definition here let's quickly go inside of alth client so it's located in the lib folder alth client i forgot that we have to add the plugins here as well so just polar client from polar better alth like this and now you can see immediately the error the type error has gone why slug pro because that's exactly what we have defined here the slug is called pro so we can now easily refer to it without knowing the id of the product and now when you click upgrade to pro wait a few seconds and you will get redirected to the payment screen if you keep seeing the white screen you can just hit refresh and it will then load node base pro this can happen when you are logged in in your polar account and you try to purchase your own product it's kind of a like a protection in some sense and in here if you want to test it out you can very simply just use stripes test cards which are these ones the expiration needs to be in the future the security code can be whatever you want and in here you have to follow the guide as you would normally purchase so i'm in croatia so that's what I would choose and you can see the taxes that are going to apply that are going to be applied and once I click subscribe here I will simply be redirected back to my product here now nothing is really happening for me here but if you take a look at here you can see the actual revenue that has just happened for this user you can see that now I am on $29 of monthly recurring revenue and how do we now use that information how do we now give this user some special access well very easily so let's go ahead for example and let's hide this sidebar menu item if the user has already purchased or subscribed to pro now this can also be done in many ways But the way I'm going to do that is by going inside of my features folder. So, inside of source, features, I will create a new folder called subscriptions. So, features, subscriptions feature, or payments, whatever you prefer. And I will create a hooks folder inside. And then I'm going to create use-subscription.ts. Let's go ahead and import useQuery from tanStackReactQuery. Let's import outClient from liboutClient. Let's export const useSubscription. To be returned useQuery, queryKey, subscription. QueryFunction, asynchronous method. and let's destructure the data from await out client customer dot state so you can see how easy it is to get the customer's state since we are connecting our internal database id with polar's external database id i mean it's transferred to polar as external database id we can very simply get the polar state by calling whatever is the current out client session dot customer dot state you don't have to pass this inside of react query this is completely optional i simply just prefer using use query whenever i can you could have very easily just done that here somewhere but i personally like it this way much more and then let's export const use has active subscription hook so we can very simply use this throughout our client site on the back end it's much easier than this because we can very simply just await without needing to refresh or something but in here we kind of have to rely on some refreshes and refetches right so let's go ahead and get the data in the remap it to customer state let's get is loading and the rest of the values all from use subscription and then let's define has active subscription as customer state question mark active subscriptions and customer state dot active subscriptions dot length are larger than zero because we only allow the user to have one active subscription at the time which makes it very easy for us to confirm whether this user should see their pro status or not. If you had a more complex situation you would have to look through the active subscriptions to find the one with the ID that you need. And then in here I'm just going to return has active subscription. The active subscription will be customer state question mark active subscriptions question mark first in the array. I will pass is loading and I will pass the rest as well. There we go. So we now have use has active subscription. So I'm going to go here in the app sidebar and I will add use has active subscription. Let me show you the import here from features subscriptions hooks use subscription has active subscription and is loading will be extracted from here and now let's finally go down here where we have the sidebar menu item and let's check if not has active subscription and if we're not loading the state of the user's subscription then show this upgrade button so now if you take a look you will see the button doesn't exist because my Antonio user is premium but if I sign out and if I sign up again with a new account I'm using the same password here so let me go ahead and sign up this new user will now see the subscription here maybe I need to do a refresh here it is yes I need to do a refresh and the reason it stayed invisible is because the previous user was premium and when I signed out we didn't clear the react query cache now you don't have to really worry about that because again this isn't our layer of protection this is the same as redirecting the user away when they are logged out this is just to improve user experience because the true premium protection will be inside of our data access layer by introducing the same way we introduced protected procedure in trpc we are going to introduce premium procedure in trpc so you don't have to worry about glitches like this in a sense that obviously it's not good to have any bugs but in a sense that this is a security leak it is not so don't worry just because you forget to clean up some state it won't mean that someone will be able to use pro features because pro features will be restricted using trpc data access layer protection so just refresh and then you should be able to see upgrade to pro here and you can also now enable portal down here so billing portal should always be active if you ask me so customer and then just call portal it doesn't matter if the user is a pro or not when you click on billing portal it will simply redirect you to your customer portal which will say no active subscriptions no benefits available right super simple but if you sign out and if you go inside of antoniomail.com or whatever you added as your premium customer in that case in here when I click billing portal you will see that I have a current active subscription and I will be able to cancel it from there as well. So here it is $29.99 per month. I can change plan. I can view subscription. I can do what I want. Perfect. So now let's go ahead and let me just show you one more cool thing. This is actually very new. What they've added is the ability to trial. So let me expand my screen here. Let me collapse this internal sidebar that they have. You can actually select a trial period if you want to. So configure a free trial period for this product. So if you enable this, you can select, for example, one month of free trial or one day one week right whatever combination you want and just go ahead and click save product and what's going to happen now is that if you go back to your app here and let me go ahead inside of sign up my apologies inside of the other account so this one one two three four five six seven eight let's see if i entered that correctly i did Let me just refresh. Okay, you can see it appears. And when I click upgrade to pro here, you will see that it says one month trial, which means that I will not be billed, right? And if I take a look at my customer portal later, it will say status trialing. So it is that easy to add a free trial to your product as well. Excellent. So I purposely want to leave one of my customers to be free so I can demonstrate creating a protected premium procedure. In order to do that, let's go inside of init.ts within source trpc folder. In here, we have already created protected procedure. So now let's extend it. Export const premium procedure will be protected procedure Let go ahead and make this an asynchronous method and let destructure context and next Let get the customer by doing await polar client which we can import from libpolar. So just make sure where is it? Here it is from libpolar. So polar client dot customers dot get state external and use the external id context out user id so that's why we are extending the protected procedure so that we have the out session here so that we can obtain the user id which is mapped as external id in polar and then in here let's go ahead and check if not customer dot active subscriptions or if customer active subscriptions dot length is equal to zero. Let's go ahead and throw new trpc error here with a code forbidden and a message active subscription required. Otherwise, let's return next and let's go ahead and pass context here, spread the existing context and simply add customer inside too in case we want to do something with the customer object and it is that simple to integrate a premium procedure so let's go ahead now and test it out quickly so i'm gonna go inside of uh let's see actually we will test it out in some other example in the next chapter simply because we now have to convert something to a client page i think it's unnecessarily complicated for something that we can very easily come back and test but i think you already understand how it works right it works the same way as protected procedure but now it's a premium procedure so inside of your app underscore app in source trpc routers if you wanted to change test ai you can change it to be a premium procedure right instead of a protected procedure and then this will throw an error if the currently logged in user isn't subscribed let me see if i can very quickly test that somewhere inside of source app dashboard rest let's go ahead and create a subscription and let's go ahead and do a simple page dot dsx use client let's go ahead and let's import use trpc and let me add use mutation here from 10 stack react query export. Let's do const page, const trpc use trpc, const test ai will be use mutation trpc test ai mutation options let me fix the typo use mutation let's return a button from components ui button click to test subscription and inside of here let's add on success toast from sonor.success success otherwise on error toast.error failed. This way we will see what's going on. And let's add on click here test ai.mutate and in fact maybe we can grab message from here and then show the message like that and let's export default page so now head to http localhost 3000 slash subscription subscription just one like this and in here you should see click to test subscription so i'm currently on a free user and if i click this let's see what happens active subscription required i get an error but if i go and sign out one two three four five six seven eight and i log in here and let me just refresh and go back to subscription and i click click to test the subscription i get back success now you might get an error here if you don't have ingest running so just make sure you have ingest running too and now ai is running in the background amazing so we successfully wrapped that up so we can now remove the subscription page here because we don't need it and i think that we developed everything we need for now. So the only thing that I kind of don't like is this use subscription which is being cached through logout. So the new user who logs in temporarily gets the premium state of the premium user. I will look into the best way of fixing this but again that's just a UI bug because our actual protection is inside of premium procedure here which will always make a separate API call to Polar to confirm whether this user that's currently logged in has the active subscription or not and it is simply extending the protected procedure here so this cannot really be hijacked by some fake ID. We are using the ID from the session that we have. So I believe that marks chapter and I hope you realized how fun it is to use Polar really. In order to develop all of this it would take me so much time and headache with Stripe and not to mention the webhook and then the local testing of the webhook. It's just you know a headache but with Polar it is just ridiculously easy to do so. So let's see we have set up Polar, we integrated with better out, we created checkout and we created the billing portal. And now let's go ahead and commit that. So chapter 10 payments, I have nine files here. You can see them right here. You might have 10 files if you accidentally have some error for mprox because it can create like an additional file. But these are the files you should have. So I will click down here on main. I will click create branch, 10 payments. I'm going to then stage all of my changes. and I'm going to do 10 payments and commit and then I'm going to publish the branch. And after I publish my branch, as always, I'm going to create a pull request so I can review my changes and see if we need to fix any critical mistakes. And here we have the CodeRabbit summary. Subscription aware sidebar. Upgrade to Pro appears only when applicable. and launches checkout screen. We added in-app billing portal for managing subscriptions. Premium actions now require an active subscriptions. Non-subscribed users see a clear error and an upgrade path. We also added the dependencies to support said checkout billing and subscription state. In here as always file by file walkthrough and in here we have the sequence diagram. So this one is explaining how our upgrade to Pro works which very simply uses AuthClient's Polar plugin to call the checkout function which simply creates the checkout session and redirects the user to that URL. The same way customer portal works. And in here we have a definition of our premium procedure. So when the user calls mutate test AI, we invoke a premium procedure, which again uses the polar climb to get the customer state using user ID. If the user has an active subscription, we proceed with whatever that procedure is of wrapping otherwise no active subscription with throw an error active subscription required in here we do have a few comments but it's mostly the things we already knew my upgrade typo and in here a guard against missing polar success url So if someone forgets to pass that, we should either throw an error or we should add a fallback. So we are going to look into that here. Same thing here, what I tried to do, dynamic polar server variable. I will see into in the next chapters how we can improve that. And it looks like that is it. So let's go ahead and merge this pull request. And once it is merged, we can go back to the main branch here as well. and we can click on synchronize changes and okay. And after that, head inside of your source control graph and just confirm that you just did 10, you detached and you merged back in. And that means everything is as it should be. And now let's go ahead and wrap up the chapter. We pushed to GitHub, we created a new branch, a new PR and we reviewed and merged. Amazing, amazing job and see you in the next chapter. In this chapter, we're going to work on Workflows, Create, Read, Update and Delete methods. In order to do that, we first have to update the Workflow schema. And then we can create the Workflow trpc API routes. Let's start by updating the schema. So head into schema.prisma located in the Prisma folder. and find your workflow model. Inside, we just have ID and name. Now let's go ahead and extend this by adding created at, which will be a type of date time with a default value of now. And then let's add updated at with the same value and a decorator updated at. And now what we're going to do is we're going to make a model belong to a user. We're going to do that using relationship and foreign keys. So let's add user ID field, which is a type of string. And then I going to expand my screen a bit because this is now a longer line User is a type of user referring to the model above which we have called user So user type of user decorator relation Targeting the field user ID, which we defined above. It's referencing to field ID within the user model. And then let's add on the lead here, cascade. So when the user gets deleted, all of their workflows will be deleted as well. That's what onDeleteCascade does. So we will never have a workflow which doesn't belong to anyone. And now that we have that relation set, we have to create an equivalent relation in the user model. So let's find the user model again. And just as we define sessions and accounts, let's add workflows. workflow and an array. And you can see that the moment you save, you will no longer be seeing any errors here. So I think that is enough for now in regards to the workflow. You can save this file. And inside of your terminal, let's do npxprisma migrate dev. And let's call this workflows improvement. or maybe workflows update something simple here we go after that you can do npm run dev all make sure your next js server starts successfully now that we have that let's go ahead inside of our source folder features and in here let's create a new folder called workflows inside of here another folder called servers and finally routers.ts inside. Let's export const workflows router to be create trpc router from trpc init and then add a create procedure here which will be for now a protected procedure from trpc init. It's going to be a mutation and in here we're going to execute a function. In the params of that function, you can extract context. And then what you can do is return Prisma from lib database dot workflow dot create, open the data object, give it the name of to do. And let's go ahead and give it user ID of context dot alf dot user dot id just like that that's all the things we have to pass in order to create a new procedure now i do want to slightly modify this by not having to write to do for the name we can do that by installing a very simple package npm install random word slugs it's a nice little package which I often use in my projects to very simply just generate random words which looks familiar to what big apps do. Generate slug from random word slugs. You can configure this generate slug function quite extensively actually but I will just keep it to three words as simple as that. Now that we have the workflows router we actually have to go back inside of our underscore app folder in source TRPC routers and we get to finally clean this up. Let's remove everything inside. So just an empty app router and just map workflows to be workflows router like this. And then finally you can remove all the unused things from here. That includes the base procedure as well as the two other procedures. There we go. And now we have just a simple create procedure here. Now let's go ahead and add the delete procedure, which I like to map as remove instead of delete simply because delete is a reserved keyword in JavaScript. So I just like, I feel safer using remove. So let's go ahead and add a protected procedure here as well, mutation. And again, we execute a function here. And let's go ahead and simply destructure context from here. And let's return Prisma.workflow.delete where and let's go ahead and do user ID matches context out user ID. So only the user who actually created this workflow can be the one to delete it. But the only problem with this where query is that user ID doesn't make a workflow unique. So we also have to add something before we define the mutation. So besides mutation, we are also going to have dot input. And that's going to be using Zod. So make sure you add z from zod, zod.object, id, z.string, like this. And then besides the context, you will also have the input. And then you can assign the id to be input.id. And this will then attempt to delete the workflow using this id and the currently logged in user. So if there is a mismatch between the two, it's going to fail. either because the id is not correct or because we don't have permission to delete this because we are not the person who created it we are not the author of the workflow so yes you can do that very easily by just properly querying inside of prisma you don't have to first query it by id and then check if the user id matches except if you want to throw specific errors but for now this is okay. And now let's go ahead and let's create a simple, very specific route, which I'm going to call update name. So update name, again, a protected procedure with an input here. So z.object, and it's going to accept an ID just as the previous one did, but also a name with a minimum of value of one. So at least something has to be passed. And again, it's going to be a mutation. Let's go ahead and destructure the context from here. And then in here, return Prisma workflow update, where and data is what we need. In the where, make the ID be input.id, which we also need to destructure from here. And then we can properly do it here. And user ID context out user ID. And the data will very simply just update the name. And just like that, we've created a secure, authorized, and authenticated endpoint to update a workflow name. I am very purposely separating just the name update because later updating the nodes will be a little bit more specific. And I think it's smarter to separate these two endpoints to keep them simple. And well, not exactly to keep them simple, but just to keep them one for each purpose. It's easier to maintain this way. And now let's go ahead and let's do get one here. So get one will be a protected procedure. and let's go ahead and just add a query here. And we can also extract the context here. And let's just do return prisma.workflow.find unique, which means it will either find this or it will throw an error. And then where we're also missing an input. So let me just copy the input from my remove procedure. because we are looking for a very specific one, right? So we need the ID. Where ID is input.id, user ID is context out user ID, like this. And then let's go ahead and copy this and change this to be get many. For this one, we don't need an input. Now, we will need it later for all the various querying, like searching and things. but for now we can just do context find many and change this so it will now load all of this users workflows so this is obviously a simplified version because we will have pagination too right now this doesn't have pagination it doesn't have anything but we will do that later for now i just want to have all the methods here. And I think this is enough for us to start testing these routes. So what I want to do now is for each of these actions, I want to create an equivalent hook that we can call. So inside of source features workflows, where we just created the server, let's go ahead and create hooks folder. And inside of here, I will add use-workflows.ts. and let's start with a very simple one, a hook to fetch all workflows using suspense. So I like to add comments like this. Export const use suspense workflows. I like to be specific because it's important for developers to know whether they are using suspense or not. So trpc, use trpc and let's go ahead and just return use suspense query. like this, trpc.workflows.getmany.queryOptions, like this. So this will now be used as a reusable useSuspenseWorkflows hook, which fetches all the workflows using suspense, which means it needs to be wrapped in the proper hydration boundary and suspense and all of those things. We are basically going to be using the third option of our, whoops, the third option,\nwhich we discussed, if you remember, in TRPC chapter, we explored TRPC with server and client using prefetch. So for that exact scenario is what we are preparing right now. We are going to use the best of both worlds, if you remember. So for now, I think that just this one actually might be enough. So I'm going to stop here because I just want to start demonstrating how we're going to use this. so let's go ahead now inside of workflows here create a new folder called components and inside of here create workflows.tsx like this and then let's go ahead and export const workflows list and let's do const workflows use suspense workflows from dot dot forward slash hooks use workflows like this and then let's go ahead and let's return a very simple let's just do a paragraph json stringify workflows dot data and just some other things here to make it format in a prettier way like this so we are later going to create this in a proper component but for now it's just going to be an indicator for any data. Now let's go ahead and render this within a workflows page. So I'm going to go inside of source app folder dashboard rest workflows page.tsx. And now in here I'm going to go ahead and prefetch my workflows. Now the way I'm going to prefetch my workflows is by creating a reusable prefetch util just like I did with my hooks. So inside of features workflows we have the server folder and let's just add prefetch.ts inside because this is relating to the server side. and let me just see i think yes one thing that we are missing here is let me just try and quickly check inside of trpc server yes we only added caller we didn't create any other helpers here so let me quickly open trpc docs and hopefully we can find what i'm looking for so because i want you to see exactly where i find my information so click on tan stack react query and then click on server components and we already followed this entire process here but if you scroll a bit down you will see these two hydrate client and prefetch so if you can find this copy and paste it if you can't pause the screen and you will be able to see what i'm talking about so you can also create a prefetch and hydrate client helper functions to make it a bit more concise and reusable so that's the purpose because very often we're going to use both of them so let's you know save some time here so in the server here i'm going to add the prefetch function and we need to import trpc query options from trpc tan stack react query like this so make sure you have that and now you have prefetch right here and then let's also copy hydrate client function for this it is very important that your server file has a .tsx extension if it doesn't change it so make sure you can pause the screen again if you don't have this The only thing we had to import was the RPC query options. And then below this, add hydrate client. And in here, import the hydration boundary from 10 stack react query and dehydrate from 10 stack react query. So let me show you hydration boundary from 10 stack react query and dehydrate from the same place. So make sure you have this and make sure you have this. again you can pause the screen if you were not able to find it in the documentation but it should be there great now that we have that let's go ahead and create our prefetch here so the first thing i want to do is i want to import type infer input from trpc tan stack react query and then i'm going to import prefetch from trpc server as well as trpc itself then i'm going to create a type input which will infer the input type of drpc workflows get many so i have the exact type that will be accepted for my get many at the moment this makes no sense because we don't have an input but later when we add an input which will have you know search and things like that it will make sense because this input will be exactly typed as we need it so we don't so we never have to repeat ourselves so let's go ahead and add a prefetch all workflows util export const prefetch workflows accepting the params which are input and return prefetch trpc.workflows.getmany.query options and pass in the params. So this now works, right? The query options don't exist. They are nothing, right? It's the same thing if I do this. But imagine I changed this to be .input here, z.object, search, z.string, something like that. You can see that now it's expecting that. But I don't want to remember which are the props that I've passed. So I created this, which will automatically infer that. You can see how it added search. So I can safely just pass params here. So even if I remove it, it still works. I hope that clears it up. It's not fully clear because we don't have any input here, but that's how you can infer the input type for any TRPC procedure that you have, which is quite useful. Because imagine if I had to manually add search and then string here, right? And then imagine I had a million of these. So it's just easier to reuse the types, even if they're completely empty and the type of void. So now we have prefetch workflows and I can go back inside of my dashboard rest workflows page.tsx here with the workflows paragraph. And now what I'm going to do is just do prefetch workflows like this. I didn't have to pass anything here, but usually this is where params would be accepted. And now that they are prefetched, I also have to properly render these by adding hydrate client from trpc server, which is another util we just added. And in here, let's add the error boundary. Now, we actually don't have this, so don't import it from anywhere. The place to import this is a third-party package. So let me quickly install it. npm install react error boundary. As far as I know, it is the most popular package for react error boundary. So let's now import error boundary from react error boundary like this. And passing the fallback here to be error. And then add suspense from react. simply because we are using use suspense workflows. So we need to wrap that component within suspense. And for the fallback here, let's go ahead and let's use loading. And finally, let's render the workflows list. So make sure you import the workflows list from features workflows components workflows. and inside of workflows list we are using use suspense workflows so because of that we can safely wrap suspense around this and if it fails it will show the error and we already prefetched this in the server component which is asynchronous and loads faster so it will have that effect that we first learned in chapter three where i demonstrate you how this is the best of both worlds and you will get very uh good results here so let's refresh workflows and we should just see either an empty array or an error as i am getting let's see why attempted to call use trpc from the server but use trpc is on the client i think what i forgot is in the workflows list i have to mark this as use client. There we go. And now let's refresh and try this again. And there we go. You saw the loading fallback and then an empty array. Great. Now let's go ahead and add an ability to create a workflow. So the way our workflows container here is going to look like is exactly the same how our credentials and how our executions container will look like. So in my opinion, it makes sense to start creating some reusable components here. The way we are going to do that is by going inside of source components and in here create entity views dot dsx or maybe entity components would be better. So entity refers to workflow, credential, execution, right? Because all of them will reuse these components. So I want to start by having one called entity header. And entity header is going to have a couple of props. So let's define entity header props. let's go ahead and define this so entity header props is an interface and it's going to accept the following props title optional description optional new button label optional disabled prop and optional is creating boolean and then let extend it even further by giving it some very specific combinations of types So if the user passes on new which is a type of just an arrow function just a void in that case, new button href should be never like this. And let me just check if I am doing this correctly. This should be a type, my apologies, not an interface. And yes, do this. so if we pass on new it means that inside of the entity header the add button as in add new workflow will be done using a function and then we can we should not be able to pass the link for the new button screen but if someone passes new button href first and it becomes a type of string in that case on new should be a type of never so it can only be one or the other and let's also do on new to be never and new button href to be never so one of them is required it's just up to the developer what they're going to choose so now that we have that let's go ahead and let's extract all of those props title description on new new button href new button label disabled and is creating and in here let's go ahead and let's return a div with a class name flex flex row items center justify between gap x of four another div here with a class name flex flex column and then an h1 element which will render the title and now let's give this h1 element text large on medium text extra large and font semi-bold and now before we do anything with the rest of the props i want to reuse this i actually meant to say i want to use this so So instead of features, workflows components, workflows.tsx, let's go ahead and let's create the workflows header. So export const workflows header will accept only disabled. And did I do this correctly? Let me just check what am I doing incorrectly here. I think it's just because I didn't like complete it yeah okay so in here let's return empty fragment entity header from entity components which we just created and in here I'm going to pass the title to be workflows like this and a description to be create and manage your workflows and and then you will see what i was talking about if you pass on a new which can be a function in that case new button href should not be passable you can see i have an error right so you can only do one of the two and if you pass new button href then you can't pass on new because this will be like workflows new. If you want that, then you will be able to pass this. So this is just like a little exercise on having good developer experience when writing your components. So for on new, I will just do this now. New button label is going to be new workflow. Disabled will be disabled and is creating is going to be false and keep it inside of fragment for now even though it makes no sense it's simply because we will add some other elements later so that's how we're going to reuse these entity headers right later when we have credentials we're going to reuse it and have the title credentials description create and manage your credentials new button label new credential so we don't have to write this entire entity header a million times right i think after you see how we're going to use this you will agree with me now let's check if we have the description so we are now completing the entity header component if we have the description let's add a paragraph here and let's render the description inside and let's give the paragraph a class name of text extra small on medium text small text muted foreground and then let's check if we have on new and if we don't have new button href that means we are intending to use this button which we have to import from forward slash ui button right because we are already in the components folder so if we have that in that case it means that plus icon should be rendered here make sure you added this from lucid react give it a class name size 4 let me just remove the extra space here and let's render new button label and now let's go ahead and give this disabled is creating or disabled size small on click on a new and just to make this more readable I will collapse these props but basically you see the point if the user passes on new then we are going to immediately execute that function when this button has been clicked and we also should have a fallback for the new button label or maybe we don't how about we just make the new button label required this way we don't have to have a fallback it will just be required for everyone and now let's copy and let's paste this and let's reverse the situation so if you have new button href and you don't have on new in that case the button will never be disabled it will not have on click instead it will have as child and we're going to use link from next link to redirect the user to an href which will be new button href and let's prefetch that so it's faster so make sure you've added a link from next link. So that is how this is going to work. Basically, if inside of the entity header, someone passes new button href, in that case, we're going to serve the button as a link to redirect to wherever we intend to. Or if we have an exact function to create a new thing, we're just going to execute that function. Great. So that will be our reusable entity header. now we have to create two more reusable entity components here but I think it would be a good idea to start to render this simply so you can see what we are doing because it's quite hard to develop this way when you don't really know what you're doing so after workflows header let's go ahead and do export const workflows container here. And inside of here, let's simply render the children, give it a type like this. And inside of here, we have to render entity container, which will render the children and give it a header option, which will be workflows header like this and then give it search which will be an empty fragment and give it pagination which will be an empty fragment as well so in order to see our workflows header we have to create the entity container first which thankfully isn't too difficult so let's go back instead of the entity components here and let's do export const entity container let's go ahead and return a div and let's import entity container here just so we stop getting the error for its existence and now let's go ahead and properly give it the props so I'm just going to copy the type here I'm going to add it here and I'm going to change this to be entity container props. Now the props will be thankfully a little bit simpler. So we can just add these. And let me go ahead and just add prefix to all of this. So it's react.reactnode. So children optional header optional search optional pagination. and let's just assign entity container props there we go let's extract all of the props children header search and pagination here and then finally in here let's render the header now let's give the top div a class name padding for md px of 10 md py of 6 height full and let's give this one mx auto mx max with screen extra large full with flex flex column gap y8 and full height like this and now that we have the workflows container which renders the entity container which we just developed here and allowed it to render the header for which we used workflows header for which we used entity header now we can finally go inside of source app dashboard rest workflows page and in this page we can now wrap the entire thing in workflows container like this import workflows container from features workflows components workflows and this is what you're going to see workflows create and manage your workflows and a new workflow button so this will now be a nice chunk of reusable code for each of these entities credentials executions right That why we are spending a little bit more time on it than we usually would, is to create a reusable structure, right? So we have this entity container which has this exact layout that we need. And if we need a very specific header, we can just pass a different header here. So now that we have workflows header, let me just see one thing. One thing that I'm noticing is that I'm no longer seeing the body here and that is because of entity container so let's finish entity container so we can actually see the body that we are passing here so after the header here we have another div with a class name flex, flex column, gap Y4 and height full. And inside of here, we should render search if it exists and we should render the children. And below that, we should render the pagination. And once you save this, you can see that we pass the children here. And when I refresh, I think that I should start seeing, oh, I'm seeing it's just right here at the bottom. so I'm not sure if that is a bug. Let me just check entity here just a second. Hard to say if it's a bug because we don't really have proper data here. Let me just change the workflows list into a div and give this a class name of flex1 justify center items center. Will this make it any better? I'm not so sure. And then in here, maybe a paragraph wrapping this. Oops. All right. Yeah, I think something might be wrong here or maybe just in mobile mode. I'm not really sure. I'm going to debug just a little bit. All right. So first thing that I can see that's missing here is another flex. So if I add another flex here to my workflows list, what changes is that now it's centered, but for some reason all the way down here, and I want it to be here. So something in the workflows header, maybe entity header here, could be taking too much space. Let me just check by adding bgred 500. Great. So header is taking only the space it needs. let's take a look at the entity container here mx auto flex flex call header all right flex flex call height full ah yes there is a bug this container here which we have added to entity container is not supposed to end here it's supposed to end at the bottom there we go okay I had a feeling something is off. I just wasn't sure because we don't have proper data here. But yes, this div, MX auto, max with screen extra large, with full, flex, flex call, gap wide, height full, is supposed to encapsulate all elements, the header, the search, the children, and the pagination. So after we do that, you can see it works nice and we no longer have that weird thing at the bottom. You can no longer scroll there. So the fix was in entity container. Make sure that this div doesn't end after header. It should end here, right? So now we can close entity components. And in the workflows list, all we did is turn this into a div and we have centered it just so it's easier to find it here. But now what I want to do is an ability to click here and for a new workflow to appear. For that, we're going to go back inside of our use workflows hook where we added use suspense workflows. And now let's create a hook to create a new workflow. So let me close this. Export const use create workflow hook. and inside of here let's go ahead and let's add three things the router the query client and use the rpc you can import use router from next navigation and you can import use query client from 10 stack query so make sure you have added all of these now that you have that let's return use mutation here and looks like we also need to use mutation from 10 stack react query just make sure you have added use mutation from 10 stack react query inside of use mutation we're going to add the rpc dot workflows dot create and let's actually collapse it like this so it's easier to look at let's pass in the mutation options here on success on success let me just properly do this on success we get the data that was just created the new workflow and let's first send a toast from sonar.success which will say inside of template literals workflow data dot name created. So it will tell you the exact name of the workflow because names are randomly generated so that you know which was the newest one. After that, it's going to push to, again, template literals forward slash workflows and then data id so it's going to redirect that newly created workflow and after that let's go ahead and do query client invalidate queries trpc workflows get many query options so we are going to reinvalidate our current state of get many so the new one is loaded immediately and on error here let's get the error and let's do toast error failed to create workflow and let's pass in error.message like so so now we have our use create workflow so let's go ahead and use it inside of the workflow header now let's go ahead and add our newly created create workflow hook use create workflow so let me show you exactly where i imported that from from hooks use workflows the same place i've imported use suspense workflows this is inside of our workflows.dsx file let's go ahead and do const handle create, create workflow.mutate, pass in undefined as the first argument and on error here, we're going to handle error a little bit specifically. So for now, let's just console error. But later, we're going to change this. Let me in fact add a to do here. To do open upgrade model. We could have technically done that directly in here but the question is you know do you want that to happen or not? Some could even argue that I shouldn't do router push within a reusable use create workflow as well because when it comes to developer experience this shouldn't have any unexpected side effects and redirecting to the newly created one actually is an unexpected side effect. So yeah, perhaps we shouldn't do it. I don't know. For now, I will leave it like this and let me just go ahead and test this, right? So I will now just pass in handle create here and is creating will become create workflow is pending. and let's try it out now so at the moment you can see that i'm using server side prefetching and the client side use suspense to load this and when i click new workflow that will allow me to both leverage the speed of server components for the initial load but allow me client side hooks for reinvalidation because this wouldn't be possible if we only use server components So when I click new workflow, there we go. You can see how it immediately updated the list of my workflows and redirected me to that specific workflow ID. And now I can see them here and they are cached, right? So you can see how fast it is when I just go back here. It doesn't have to load them. Let's try again. Immediately works and immediately the list has been updated. Amazing. So there's one more thing I want to do to wrap up this chapter simply because it's already been 45 minutes. And that is handle this error right here. So let's do that by doing the following. Instead of source components, let me extract this. let's go ahead and create upgrade model.tsx let's mark it as use client and let's import all the alert components from chat cnui dialogue action cancel content description footer header and title so we have all of these from chapter one when we added all components from chat cn And let's also import auth client from lib auth client. Let's create the interface upgrade model props, which receives open and unopen Boolean change. And then finally, let's export our component upgrade model. So it will accept these two props and it maps them to this. Let me collapse it so it's easier for you to understand the structure. there we go now instead of this upgrade model we are very simply just going to return all of the alert dialog compositions So alert dialog itself with open and unopened change props Then we're going to have alert dialog content, then alert dialog header. Inside of that, we're going to have alert dialog title with a text upgrade to pro. After that, we're going to have alert dialog description. and in here just some text. You need an active subscription to perform this action upgrade to pro to unlock all features and then let's go ahead and let's add alert dialog footer but we're going to do that outside of the header here. So alert dialog footer let's add a cancel action first alert dialog cancel and lastly let's add the upgrade action. So alert dialog action with on click which calls out client dot checkout with slug pro we already had this exact scenario inside of our app sidebar so if it worked here in the sidebar it should also work here just make sure you didn't misspell the slug and that's it that is our upgrade model now that we have that Let's create a accompanying hook here. So I'm just going to go instead of source, hooks, and I will create a new hook, useUpgradeModel.tsx. I will import tRPCClientError from tRPCClient. I will import useState from React, and I will import upgradeModel from componentsUpgradeModel. Let's export const useUpgradeModel. Let's define open set open from useState and set it to false by default. Const handleError will receive an unknown type of error. We are going to check if error is an instance of trpcClientError and if error.data.code is forbidden. In that case, set the model to open and return true. Otherwise, return false. And do const model, upgrade model with open prop and on open change prop to be set open. And return handle error and model. like this. A very simple use upgrade model hook. And now we can finally go back to our workflows.tsx. And in here, in the workflows header component, after use create workflow, add use upgrade model from hooks use upgrade model extract handle handle error and model itself and now finally you will see the use of this fragment below entity header render model actually maybe above it would be a better semantic decision and instead of this you're just going to pass the handle error like this. And you can see how the type is working, right? Perfect. So now this will still not do anything unless you actually go inside of workflows routers. So server routers.ts, find create and change protected procedure to premium procedure. so now not only logged in users but also only premium users otherwise forbidden code will be thrown and then inside of use upgrade model we're going to check for forbidden code thrown and open the upgrade model so make sure that you're testing this out on an account which is not premium so let me go ahead and check let me log in I'm going to refresh just to make sure no cache is left and when I click new workflow I should now see an error active subscription required and upgrade to pro text and when I click upgrade now I am redirected to the checkout page amazing and let me just confirm this on my premium account here so I know that it is working as expected. New workflow and this should just create the new one in redirect me perfect and this entire time I've been thinking about should we redirect the user or not and I think the right decision here is to redirect them in here so let's separately handle on success here passing the data and let's go ahead and do router.push workflows data id and add the const router use router from next navigation here from next navigation and now go inside of use create workflow inside of your use workflows hook in the workflows feature folder here and remove the router from here and remove the push from here this way this use create workflow has no unexpected side effects so when you specifically want to redirect you will be able to extend on success once again just like we are doing here so both of this use create workflow we already have on success and on error but since we return the entire use mutation it allows us to add very specific on success and on error for very specific use within workflows header so this way if i create a new workflow within the header which i'm doing right now the behavior is exactly the same but maybe sometimes i will have new workflow buttons somewhere where I don't want to redirect. And then I very simply just won't have this. Maybe somewhere I don't want to open the premium model. So I just won't have this. I think this is a better decision. I think this is a better way to do it. And I just reordered this by length because I like it this way. I think it's time to end the chapter here. It's 52 minutes long already. Amazing job you've been doing here. And I know this seems a little bit unnecessarily complicated, but trust me, later when you see how quickly we are going to create credentials and executions by just reusing everything that we just did, you're going to be so thankful that you went through this and created reusable components. So let me go ahead and end here, regardless of what I wrote that we should be doing. Let me go back here. So we updated the workflow schema. We created workflows API for create. read, update, and delete. 11 workflows crud. So let me go ahead and create a new branch here. 11 workflows crud. And let me go ahead and quickly review my changes. I have 14 uncommitted files. You might have 15, depending if you got any mprox error log files. So that's fine. Package log, package JSON schema, migration, and all of the files we have modified. So I'm going to do 11 workflows crud commit. Commit. Oops. I first had to stage all of them. And then commit. And then publish branch. Perfect. And now that we have that, let's go ahead and let's review all of these changes. This was quite a large pull request, so it's definitely a good idea to have another set of eyes look at it before we merge. And here we have the summary. New features. We added a workflows area with list and one-click creation. We redirect to the new workflow on success. Faster page loads via server prefetching, exactly, and client hydration, so we populate the cache using the prefetch. UI wrapped with loading and error boundaries. upgrade to pro model prompts when an action requires a subscription thanks to our premium procedure data access layer protection workflows are now tied to your account with automatic created updated timestamps referring to our updated schema some refactoring regarding the trpc endpoints and some dependencies for name creation as always file by file summary but here we have the sequence diagrams. I think the more interesting one here is how the user fetches the workflows. So when we go to workflows and once we confirm out, we call prefetch workflows. The prefetch workflows then does get many, returns the workflows, and it does cache fill. And once we render the client using dehydrate and with the hydration boundary we can do something called hydrated render which is basically the super fast data load that you are seeing when you use a combination of prefetch and we do that finally with use suspense and we serve the cached workflows result. That's why it's faster than normal. And in here we have a diagram explaining how create happens, more specifically what happens if we're not premium. So if we're not premium, we throw forbidden error, we handle that error, and we then open upgrade model if forbidden has been thrown. We already understood this, but it's always nice to have a diagram here as well. And it's so impressive how CodeRabbit knows exactly what were the two most complicated routes here. What were the two most complicated jobs? And it decides to create a diagram of those two. In here, we do have some comments. The first one is regarding the migration file. Since migrations are using the dev command, I don't really take these seriously, because obviously in production, you would do migrations in a proper way. The reason this is\ncritical is because yes we are dropping all workflow records whenever we modify something so yeah in development we are doing that in production we should not be doing that's a completely right comment here and this is where it's actually wrong and i haven't seen any ai get this right yet simply because prefetching is kind of a new concept especially in trpc and tan stack query. Adding await to prefetch workflows will do absolutely nothing. In fact, you can see inside of trpc server, this little helper that we have for prefetch, it's not using await, it's using void. The reason it's using void is so when you use prefetch yourself, you never actually expect anything to be returned. So let me show you page.tsx in the workflows. It is used so you never do things like this, right? This will never work. Workflows will always be a void, right? Because people were struggling to understand what prefetch workflows does. So that's why this is actually incorrect. Our 18 prefetch workflows will do absolutely nothing. And in here, it's actually a good comment. We can use shorthand operator because prop will be automatically passed, and we don't have to use three lines for it then. So yeah, that's correct. And in here, let me see. Okay, so yes, technically, this is correct, because it noticed that we don't have any params passed in the prefetch, but we accept it here. We will fix that later when we actually have some params for get many and they will not be optional. They will actually be required. And this is another thing where I'm pretty sure the AI is wrong because I also had Claude code and GPT tell me this, but I'm pretty certain that you can use find unique with a composite query like this. I think that Prisma would throw an error if we tried to use find unique with a property that doesn't make it unique. So in here, it's telling me that I should switch to find first and cloud code and, you know, cursor did both of that when I was developing this, but I'm fairly certain all of them are wrong. I think in newer versions of Prisma, you can very safely do this. We both tested that it works and there were no errors being thrown inside of our IDE nor the server. So I will keep my eye on this because it's telling me that composite where clause will fail for find unique, but I'm very certain that that's not the case anymore. Perhaps it was the case in the past. But now I think you can pass a composite query with one unique field and one authentication field. I mean authentication field, one other field for authentication. So I think find unique works perfectly fine here and it's not needed to use find first. The reason I recommend using find unique is because I think it relies on indexes more than find first and it will then be a better and smarter query. So I will definitely research this to make sure I'm not telling you incorrect information because we will be doing this a couple of times within the project but I think we just proved by finishing this chapter that it works fine. So let me go ahead and click merge pull request and let me confirm a merge and once we have done that let's go ahead and switch back to our main branch here and then let's go ahead and hit the synchronize changes and the moment you click that everything should be updated here. There we go. Perfect. Amazing, amazing job as always. You can check the graph to see that you just detached chapter 11 and then merged it back here. Perfect. That means everything is fine. and then to wrap it up let me go back here and just mark this as completed. Amazing, amazing job and see you in the next chapter. In this chapter we're going to continue working on the workflows entity focusing on pagination and search. Before we go into that I want to resolve one question from the previous chapter's pull request review and that was can we safely use find unique with composite where clauses because code rabbit and many other ai models have warned me against using this to the point where i wasn't even sure myself if this is something i can teach you and i'm happy to say that we can use this safely. The thing is, this is a very new thing. As you can see from version 5.0.0, the generated type unique input on where exposes all fields on the model, not just unique fields. So you can see that previously that was only available under the extended where unique preview flag. So here's the rule. You must specify at least one unique field in your where statement outside of Boolean operators. And then you can specify any number of additional unique and non-unique fields. You can use this to add filters to any operation that returns a single record. For example, you can use this feature to do permission checks, which is exactly what we are doing. We are adding alongside one unique field, a user ID field, permission check. Can we fetch this? Are we allowed to fetch this? So the reason all AI models are throwing this error is because this is a relatively new thing. So yes, I'm happy to say that we can safely do that. And now let's focus on adding search and pagination to our workflows. So what I want to start with is creating a config file folder, actually. So instead of source, let's create config. And inside of here, constants.ts. Let's export const pagination. Default page will be one default. Well, let me actually show you all the options. Default page size will be 5, maximum page size will be 100, and a minimum page size will be 1. Great. Once we have that, let's go inside of routers.ts, inside of source, features, workflows, server folder, routers.ts. So in here where we have the create procedure, remove procedure, update name, get one, and finally get many. so let's start by adding dot input here for the get many procedure and let's open z dot object and then inside of here let's start by adding the page so what page are we on of this get many procedure that's going to be a type of number with a default value using our pagination so pagination make sure you input it from config constants let me show you how it looks like this and then you can use the default page which is set to one like that then let's add page size page size is going to be z.number and then it's going to have a minimum and a maximum value a minimum value of pagination minimum page size and a maximum of maximum page size and let's also give it a default of default page size, basically all the constants that we have here. And lastly, let's add a search, which is z.string with the default of just an empty string. And then in here, besides context, we can also export input. Let's go ahead and make this easier for us by destructuring page, page size, and search from the input. And let's go ahead and turn this query into an asynchronous query because we are no longer going to directly return this. Instead, what we're going to do is we're going to get items and total count using await promise all. And inside of here, open an array. And now the first one in here is going to be our existing Prisma workflow find many. and then the second one will be prisma workflow count like this and let's just add where user id context out user id so only count how many total workflows belong to this user and then in this where we're going to focus on some more specific things so right now both queries look identical. This one uses count and this one uses find many. So then the total count will have the result of this and items will have the result of this. Now let's go ahead and add some pagination properties. Skip will be page minus one times page size. Take will be page size. Where will be user ID and name contains search mode insensitive. When doing pagination, it's also important to define order by. So let's do updated at descending. Like this. And I think this will be enough for pagination. now let's go ahead and actually do some math which will define what is the next page is there a next page and things like that so after the promise all is executed const total pages we're going to use math.ceiling total count divided by page size hasNextPage will be defined if the current page is less than total pages. HasPreviousPage will be defined as page is larger than one. Now let's go ahead and add a comment. Map, actually, we don't really need to do anything yet. I think for now it enough that we just return the following items is just going to be items Page will be page page size total count total pages has next page has previous page And we can also do the same for items in that case Later, we're going to do something specific with the items. That's why I'm a bit confused. Basically, we're going to have to map them to their actual nodes and thing like that. but for now let's just keep it simple and I think that's enough information for us to have proper pagination here what we have to do now is we have to actually pass the props because if you take a look at page.tsx instead of source app dashboard arrest workflows you will see some errors in the prefetch if you take a look at the workflows list you will and go instead of use suspense workflows you will start to see errors here. So let's go ahead and close everything so it's easier to understand what we have to do next. So I have actually outlined that here. What we've just did is we updated get many for procedure. What we have to do now is add nooks for param handling and we have to handle both client side and server side param handling. So we are going to store the state of our params in the URL and there is no better choice for that than the NUCs package. So using the link on the screen, you can visit the NUCs homepage and the documentation page. And the way it works is it basically allows you to control URL state the same way you would normal use state. And that goes for the reactivity part as well, which is absolutely amazing. It's super easy to install. All we have to do is npm install NUCs and then we have to add a provider. so let's go ahead and run npm install nooks here and since we are using next js you can even see which versions are supported and i will also show you my exact nooks version so package json nooks 2.7.1 with my next version 15.5.4 and now let's go ahead and go to the adapters so we are using next js app router so let's go inside of our layout.tsx let me show you where that is located in since i have a small screen here so you can see inside of source app folder the main layout so source app folder layout where we define the toaster where we import globals where we have the fonts where we have the metadata right where we have html and body defined that's the main layout So go here and this is where we add the NUCs adapter. So I'm going to add it after trpc react provider, NUCs adapter. And let me invent these two and you can see the full import here for the NUCs adapter. So I will go here and import NUCs adapter from the NUCs package forward slash adapters forward slash next forward slash app. this is how it looks like in one line. And once you've done that, I think you're ready. Yeah, you don't have to do anything else here. I suggest that you keep the Nooks page open simply so you can look through the documentation as you learn. And now what we're going to do is we're going to actually define all the params that our trpc router is accepting. So I'm going to go inside of source features workflows and in here I'm going to create params.ts. I'm going to import parse as integer and parse as string from nooks forward slash server. I'm going to import pagination from config constants. The reason I'm importing from the server, you can also import from here, the reason I'm adding the server prefix is because we're also going to reuse this params object for our server side rendering right for our prefetch so because of that we need to use this solution because server can be used both in client and server whereas nooks is client so it can only be used in client so since i already know what we are going to be using this for right in page.tsx in the workflows i already know that i'm going to have to load the params here somehow And I can only do that if I think in advance and define the params using NUC's server. And I don't want to just magically tell you this information. You can definitely find that here in the server side usage. You can see NUC's server, right? So to parse search params server side, you can use a loader function. That's what we are going to create in a second. but you also have to import the search params parse as float, parse as string, parse as integer from the same thing and you can see some examples here how that's going to look like. So yes that's why I suggest that you you know have this open just so you don't blindly accept what I say you know try and find exactly what I'm talking about. So let's export const workflows params, the page is going to be parse as integer with default pagination dot default page with options clear on default set to true. What does that mean? For example, if we had, let me go ahead and add a comment here, localhost 3000 workflows page, and it's on number two. That's pretty normal, right? We know what this means. But if the default page is number one, this is pretty useless because it's exactly the same as this. so for that reason we can tell nooks that if you hit the default page which is one just remove it like this clear on default that's what that means or if you had a search of antonio right and then imagine you cleared the search without this option it would be this which again is dumb because that's the same thing as this. It just looks bad in the URL. So that's what we are doing. That's what clear on default does. And let's go ahead and duplicate this. The next one is going to be page size with default value default page size. And the last one is going to be search which is going to be parse as string. and it's going to not use any default, just an empty string, with options clear on default, true. So basically, params can be anything, right? Just because in the URL we do localhost 3000 page 1, we don't really know that this, is this supposed to be an integer? Is this supposed to be treated like a string? We don't know really. So by using this strict definition parse as integer parse as string we are telling nooks how to do type safety for that specific param. That's why we have different definitions because page and page size should definitely be treated as numbers whereas search should be treated as string. Perfect. Now that we have that we have to create a hook called use workflow params. Instead of workflows, let's go inside of hooks, use, whoops, hooks, use workflows dash params dot ds. And it's going to be pretty easy now. So use query states from nooks. We don't have to use forward slash server because this is obviously a hook, meaning it's going to be used client side but let's import workflows params from our shared params and very simply export const use workflows params return use query states and pass in workflows params so what will this do now. Now that you have that defined, go inside of your workflows.tsx instead of components here, and then go inside of use suspense workflows. And in here, what you can now do is you can extract the params from use workflows params like this from its neighbor. And then in here, you can pass the params and you can see how they are immediately type safe because if you hover over params it accepts page page size and search and our query options accept page page size oh sorry this is what it returns uh i'm not sure if i can see what it accepts here i think i can input page, which is a type of number or undefined, page size, same, and search, which is a string or undefined. So we successfully mapped the trpc input with use workflow params. And the reason I did this through NUCs is because now it will be super easy to manipulate with this. Basically, if you have any kind of URL state, please use NUCs. It will be such a lifesaver for you. And the best part of it, honestly, is reactivity, right? So you could add set params here. And then if you wanted to, for example, we are going to use this for pagination. The way we are going to trigger next page is very simply going to be by changing the page param to number two or to number three and that's automatically going to refetch our trpc get many endpoint so we are never going to directly refetch it we're just going to change the param but in this case we just want to get the value of them so we pass them here and that is all we actually need right now instead of this query options for use create workflow, we can just pass an empty object because we don't really care for, we are not invalidating for a specific state, we are invalidating for all of them. So we can just pass an object here to get rid of that error And now that we have used workflow params we have to handle the same thing for our page which is still throwing an error here because it's also expecting some params, right? So the way we're going to solve that is by creating the loader. So instead of server here, folder, let's create params.ts. so okay it's named the same params dash okay let's call it params loader dot ds so params dash loader dot ds import create loader from nooks forward slash server import workflows params from dot dot slash params export const workflows params loader let me fix the typo workflows create loader and pass in workflows params now that we have the params loader here let's go inside of our react server component where we prefetch to workflows so inside of dashboard arrest workflows page.tsx and now we have to pass the props here. So the first thing we have to do is create a type props for this page search params which is a reserved type for each page file in Next.js and it is a promise and the type inside is going to be a type of search params which you can import from nooks server so let me show you search params from nooks server and you can specifically just import it as a type so search params is a promise exactly the same way when you go inside of like individual dynamic workflow id page params is a promise so this is params is a reserved type for each page.tsx the same way search params is for queries so in here we are using this generic search params type usually you know you would very clearly define search which is string or undefined page size which is integer or undefined blah blah and you would have to be super specific and just match that one by one with your trpc router it's just a mess right but thanks to this we can make it better thanks to nooks it's much easier so just get the props here extract search params and then in here const params are await workflows params loader from features workflows server params loader right here and pass in search params from this server component and this will then transform it into our expected type but keep in mind that this will not fail if it doesn't match the value I think it even says that here this async overload makes it easier to use against the search params I think somewhere there is a warning that it won't fail you shouldn't basically use this as a validator that's what I'm trying to say if that was your plan but I think that now it should work just fine if I refresh here we should just see as usual but if I go ahead inside of my config.ts file instead of, my apologies, my constants.ts file. And if I change the default page size to one, meaning only one result from the first page, you can see that I only get one item inside page size one, total count four. So I have four workflows in total and I have four pages in total because for each page, I have made it so that it can only be one record. So let me change this back to five. And now you can see there is one page only because I only have four of them. And for each page, we're going to display five of them, which means that in page one, all of them were created. Amazing. Pagination is working just fine. And if you go ahead, maybe you can do this. So if you want to constants, change this again to one. and then go ahead and change your URL to the following, page 2. So localhost 3000 workflows page 2. I'm going to go ahead and paste that. And you should just see that you are now on page 2 here, right? Whereas if I go manually to page 3, you will see you are on page 3. If I go back to page one, the URL should say page one. There we go. And also notice how this time, I know it's very small. You can't see. Unfortunately, I don't know how to zoom in the URL part. But it still says page one. But when we actually use the hook, use search params, and if I switch to page one using that, it would remove the prop altogether just using the default version. All right. So once you've confirmed that works, we are ready to implement the search. So let's go ahead and do that. So let's start by going inside of source components, entity components, and let's start developing the entity search component. So after entity container, let's go ahead and define interface entity search props accepting the value on change with the value prop and an optional placeholder. Then let's go ahead and let's export const entity search. Whoops, like this. And let's go ahead and assign all the necessary props inside of here. value on change placeholder and set it to search by default if it's not passed and in here return a div with a class name relative ml auto then add a search icon which you can import from lucid react give it a class name of size 3.5 absolute left 3 top 1.5 minus translate minus dash y 1.5 text muted foreground. This will position the search icon in the beginning of the input, which we are going to add now. input should be imported so dot slash ui input the same way we added the button and in order to make space for the search icon give it a limit on the width so let me just move this up 200 pixels bg background shadow none border border and pl of 8 so we'll leave some space on the left side of the input so the search icon can render. And now besides the class name, let's go ahead and pass some more attributes here. So let's pass in the placeholder prop to be placeholder. And then let's add value. And then let's add on change. On change event target value. Like this. So now that we have entity search, let's go ahead and go into our features, workflows, components, workflows.tsx, and let's export const workflows search like this. And let's return entity search from our entity components. So, so far we've imported entity container, entity header, and now entity search from here. Let's pass in the value to be an empty string, on change to be an empty arrow function, and placeholder search workflows. now that you have workflows search you can find workflows container and simply render workflows search here as a self-closing tag and now you will immediately see it right here but now let's go ahead and make it work so i'm gonna go ahead now and grab our params const params set params from use workflows params and now I can set the value to be specifically search value for example but before I do that I actually want to develop my own function for changing this value using debounce so it doesn't spam the server so I'm going to go inside of hooks and I will create a new file called useEntitySearch.tsx. So first things first, let's import useEffect and useState. Then let's import pagination from config constants. Then let's define the interface useEntitySearchProps. T extends search, which is a type of string, and page, which is a type of number. So params is a type of generic. Set params uses the generic. And debounce is an optional config how much to debounce for. Now that we have that, let's export function useEntitySearch. And let go ahead and pass the generic here t extends search which is a type of string page which is a type of number let go ahead and extract params set params and the bounce in milliseconds to be 500 let's return use entity search props and pass in the t generic here and open the function finally and this should be debounce milliseconds. Now instead of use entity search, let's first define the local search using the current params dot search value. Local search and set local search with the default value of params dot search. Now let's open use effect. We are going to use this to create the debounce effect. First, let's see whether we actually have to do it. If local search is empty and if params.search is not empty. So this means if new input has been cleared. So currently in the URL, there is a search like Antonio, but user just changed that inside of this input to be an empty string it means we can just reset everything. So set params, spread the current params so we preserve if there was any pagination here or something else and simply reset search here and change the page to be pagination.defaultpage because every time you search, I think there is kind of a practice to reset the pagination. I'm not sure. If you don't want that, you don't have to do it, but I kind of expect that as a user. And make sure to return here to break the method from going any further because what we do further is we define a timer for debounce. Set timeout. If local search is different than what we currently have in the URL, what's the current search, right? So only for new queries, set params. search new local search and reset pagination. Again, this is what I expect as a user. If I search for something and I am on page 50, I would expect pagination to reset. Like I'm trying to look for specific results again. At least that's the way I think of it. I don't know. And let's go ahead here and pass debounce in milliseconds. Now that we have this, let's go ahead and return clear timeout and pass in the timer. So we don't have any overflow in case this component unmounts. In the dependency props, pass in the local search, params, set params, and debounds in milliseconds. And last but not least, another use effect here. Set local search params.search. Params.search in the dependency array. And finally, return search value, which is local search, and onSearchChange, which is set local search. and we are going to reuse this for every single entity search with safe debounce. So now let's go ahead inside of the workflows search here and let's go ahead and use it, use entity search, which I've just imported from Hook's use entity search, pass in the params, set params, and you can now extract search value from here and on search change which will be debounced and you can now pass the search value and you can now pass on search change. Perfect. So let's take a look now. I will go inside of my constants.ts and I will change my pagination default page size back to number five. This way I see a lot of my workflows here. If you don't have one, just click new workflow to generate it. Go back to workflows. Let's find a specific name, flat curved horse. Let's go ahead and search for horse here. You can see that it's loading and then it will only return the item whose name is flat curved horse. And if you take a look at my URL, it is localhost 3000. workflows search equals horse exactly as i expected and if i delete this you can see that the url is now reset to this it didn't do this which would be bad instead it noticed that is the default i can just reset and clear the url so it looks better perfect and you can see how there is ad bones right so it doesn't query for everything only when i stop typing for half a second and it returns for this one no items right amazing you have now created uh this very amazing search and the best part it is also cached you can see how the second time i searched for horse it immediately returned the results because it was already cached for this specific query That's the power of combining TRPC and 10 stack query and NUCs. You get type safety, you get caching, you get speed, you get maintainable code. That's why I really, really like this stack. This is another reason why I feel more confident using TRPC as my data access layer as opposed to built-in server actions. I just feel I know how to use this better. I feel I know how to make better apps using this method. And now to wrap the chapter up, let's also add pagination functionality. So we're gonna go inside of source, components, entity components. And just as we've added entity search, let's go ahead and add entity pagination, starting with the interface and the props. Page, total pages, on page change, which accepts page and optional disabled. And now let's go ahead and let's actually define the component with the props like this. Entity pagination which uses all of those props and the type right here. And now inside of here let's go ahead and let's return a div with a class name flex items center justify between gap x to full width. Another div inside with a class name flex one text small text muted foreground. And now let's say what page we are on. So page, then the page number of total pages or default to number one. So page one of two, page two of two. Then another div here with a class name flexitemscenterjustifyend space x2py4. And inside, let's use a button. we should already have button imported from .uiButton. The first button will be Previous. And the second button will be Next. The first button will be disabled if page is number one or if we have for some reason disabled pagination overall. The variant is going to be Outline, size will be small on click for now will be an empty arrow function and then we can copy these props and paste them here into the next button but with a slightly modified logic if page is equal to total pages that's when the button will be disabled or if total pages is equal to zero or if we have disabled altogether. Now let's add the actual on-page change, which we can use right here. So on-click here, let's call on-page change math.maximum1 page-1. So we can't go less than the page number 1. And make sure that this is like this. And in here we're going to do the opposite. Map.minimum of total pages and page plus one. So we can't go more than the total pages number. There we go. And we are going to decide what on page change is from the outside. So now that we have entity pagination finished here, let's go ahead inside of features, workflows, and let's go inside of components, workflows.tsx. And just above the container here, so we leave the container last, we can develop the pagination, export const workflows pagination. const workflows use suspense workflows const params set params from use workflows params return entity pagination make sure to import entity pagination the same way we imported entity search and all the other ones right and let's go ahead and disable it if workflows is fetching let's go ahead and add total pages here to be workflows dot\ndata.totalPages, page to be workflows.data.page, onPageChange to get the page, and set params using Nooks, preserving the existing params, and just changing the page. So if we have a search query and then change to the next page, we should not reset the search query. We should keep it. And now that we have workflow spagination, we can render it here as well as a self-closing tag. There we go. You can see that since I have typed in horse, that's the only existing query, so page one of one. But if I clear this, I still get page one of one. Okay, not too useful of an example. we can very easily change this by going inside of our constants and change the default page size to number one and refresh immediately you can see page one of five with the next button active so if i click next there we go page two of five and i am now on page two and notice my url this is my url and yours should be to page two and if i click previous to go back to page one you can see that now the url is properly reset to default right this is my url now it didn't go to page one because it doesn't make sense page one is the same thing as this that's what clear on default does in the search params nooks configuration amazing and if i search for horse for example in that case uh let's see what happens so horse is here and i have one of five but i can still click on next oh and that just uh makes an empty page okay ah i see i see what we did wrong we have a bug inside of proceed routers.ts get many when i do a total account i don't take in consideration the name the search there we go so make sure you add this to count and now you can see now it's a bug page two of one so let's try this again and let's click horse when you search horse now it will give you page one of one but if you remove it will be page one of five and you can also see that this is cached too right so only the next ones are going to be slower but the previous ones are cached so they're super fast amazing the pagination is working uh the search is working let's search for pencil there we go page one of one previous and the next disabled the url is working amazing amazing job you just developed a kick-ass pagination and search with debounce available to reuse for all of our other entities let's bring this back to five so we have a more realistic example for future use cases and now let's merge this. So I have 13 unstaged files. You might have 14 as always in case mprox leaves some logs. I'm just telling you that in case you are comparing to yours and are wondering where did you get the extra file, right? Perfect. So let's go ahead and see if that's all we wanted to do. That's all we wanted to do. 12 workflows pagination. So I'm going to go ahead and start a new branch, create new branch, 12 workflows pagination. I'm then going to stage all of my changes and add a commit 12 workflows pagination. And I'm going to commit and I'm going to publish the branch. And as always, once the branch has been published, I'm going to go ahead and open a pull request. This was also a pretty large one, so it will definitely be a good idea to have another pair of eyes take a look at it. And here we have this summary by CodeRabbit. New features. Workflows list now includes a debounced searched input. We added pagination with previous and next controls as well as page indicators. Page, page size, and search are synchronized to the URL for easy sharing and persistence. Yes, when you add state to the URL, you can also very easily share that state if you ever modify this to have an organization level. Improvements. Workflow data prefetching now respects current filters for more relevant initial results. Exactly this way, we are prefetching exactly what's in the current state of the URL. So if user refreshes on that page we are going to prefetch that exact state. As always file by file walk through here and here we have the sequence diagrams. So not need to you know look at this too much simply because we've already went over this. When user navigates to workflows what's different now is that before we call prefetch we also create the loader with the current search params. So this is a specific example if we have search and if we have page we're going to create the loader using NUCs and NUCs adapter and then we're going to parse those fields into page, page size and search which are compatible with prefetch workflows params which are inferred to trpca workflows get many input and then that will properly prefetch from the database using the proper count, using the proper search using the proper page. Perfect. And down here we have an example of how debouncing is working, but I think we are all already aware of how that works. If for whatever reason you are curious, feel free to now pause the screen and take a look at how this is executed here. We do have a few comments here. So first one is invalid NUCs dependency version. I think this is not true. I'm pretty sure that this is a completely valid version of NUCs that we have here. In here, it suggests adding area label for the placeholder to improve accessibility, which we could definitely add. Yes. So area label for the placeholder makes sense here. In here, we forgot to add the bounds validation for the page parameter. So we add default but we never limit the minimum value. So we could definitely add that. So users can't query by negative pages. And in here we have a potential issue and a serious one, potential infinite loop because we use params object in the dependency array. So the solution here could be to restructure, to depend only on specific fields, as you can see, params, search params page. The problem is we want to preserve the existing params here. So I will have a look at how we can improve that for the next chapter. Though we tested it and it didn't cause any infinite loops. So we can also say that it works. But yes, this is a good thing to reconsider. Let's go ahead and merge this pull request. and once we have merged this request let's go ahead and take a look if everything is as intended so I'm going to change this to my main branch and then I'm going to click on the synchronize changes button and then I'm going to click on my source control here I will open graph and in here you can see that we have properly merged 12 into main. Amazing. And now let's go ahead and wrap this chapter up. So we have added nooks for param handling both client and server side. We added more entity components as well as full UI for search and pagination and we've wrapped it all app by reviewing our pull request. Amazing, amazing job and see you in the next chapter. In this chapter, we're going to improve the UI of our workflows entity by adding a set of reusable components. Loading, error, empty, list, and item. Let's go ahead and start with the three states first as they are the simplest to do and to demonstrate. Make sure you have your project running. I'm using npm run dev all to run both processes at the same time. And make sure that you are on your workflows page. Once you are in here, let's go ahead inside of source components, entity components. So the last thing we developed here was, I believe, entity pagination. And now we're just going to add a couple of simpler ones. So all of these three states, loading, error, and empty are going to share one common prop. So let's create a reusable interface called state view props with an optional message. And then let's extend state view props for each specific state. For example, loading view props extends state view props and it adds the entity option. So let me show you how that looks like in one line. And then let's export const loading view. Now let's go ahead and destructure the props and assign them to this loading view props. So we're going to have the message and the entity. Let's set the entity by default to be items if not passed and let's pass the message separately like this. Then let's go ahead and let's return a div element with a class name flex justify center items center full height flex one flex column gap y4 and let render the loader to icon inside from lucid react so just make sure you imported this from lucid react let's go ahead and give the loader icon a class name of size 6, animate spin, and let's do text muted foreground. Then in here, a paragraph with a class name text small text muted foreground. And let's go ahead and either render a message or if the message was not passed, let's just do loading and then entity, whatever entity was passed. Three dots. As simple as that. That is our loading view. Now let's go ahead inside of features folder workflows components workflows.tsx and let's go ahead and export const workflows loading to very simply return loading view from our newly created entity components export and let's give it entity workflows like this. Now that we have workflows loading let's go inside of app folder dashboard rest workflows page.tsx and let's go ahead and render it here instead in the suspense. Workflows loading and make sure you add the import. So, so far we've imported workflows container, workflows list, and now workflows loading from features, workflows components, and then the workflows export file. There we go. So now if you go ahead and refresh, you should see a nicer loading state. It's very brief, but you can see it right there. And in fact, I want to modify it ever so slightly by going back instead of entity components and maybe changing the text of the loader here to text primary this way it will be an orange color let's see let me do a hard refresh here it's cached so i can't see maybe if i try searching really not helping my example here okay yeah we were just able to see it for a second I think it might look a little bit better with that orange color I don't know you choose for yourself and now for the message you know what I think it might be easier to just send a message here instead and just render this conditionally if we have a message then just go ahead and display it and whenever you use this operator also make sure to turn this into a boolean otherwise this will render if it's truthy so add double exclamation points here or you can use the boolean operator or you can use the ternary operator with a question mark and the else right so i personally prefer this i'm used to it but whatever it's easier for you and then inside of the workflows.dsx let's pass a message here loading workflows i think this gives us kind of more explicit controls again this agent mode how do I close agent mode like that okay and it should behave exactly the same now again I'm having trouble showing it let's go ahead and continue by focusing on some other entity components now that we have the loading view let's go ahead and copy the loading view and the interface. And let's paste the entire thing here. And let's change this loading view to now be error view. And at this point, we actually, we don't, we no longer need the entity here, we can just reuse state view props. Because yeah, we just removed the entity completely so we can do the same thing here just reuse the state view props everywhere it's the same and instead of this we're going to do alert triangle icon from lucid react so the same place where we imported a loader to icon a moment ago and make sure to remove animate spin from here so how do we trigger an error well very easy actually first let's go inside of workflows.tsx and let's go ahead and do an export here. So this will be workflows error. Instead of loading view, it will be error view. And this will be error loading workflows. Make sure to import the error view from components, entity components. And then inside of the page file, also add workflows error. and make sure to use it in the error boundary. It's a self-closing tag like this. In order to break it, go inside of workflows list and just throw new error here. There we go. Error loading workflows. And then remove this. Perfect. So now that we have that, let's create an empty state. The empty state will be slightly different and we're actually going to be using some new Shatsian components, which I'm very happy about. So instead of entity components folder here, go ahead and add all the components from empty. So you can import it like this, or you can just use this because we are in the components folder. Empty, empty content, description, header, media, and title. In order for this to exist in your UI empty, you have to make sure that you use the proper npx chatcn 3.3.1 version because it's a very new component and I can actually quickly show you that uishatcn.com. If I go inside of the components here, you can see that empty is highlighted as new and it will give us this nice little no projects yet example. So that's what I want to do here. so using this empty let's go ahead and now build it I want to do it down here simply because all of my other states are down there so my empty is going to be called interface empty view props and this one will actually extend it so extends state view props and it will add on new which is an optional component. I mean an optional prop here. So it makes sense for this one. Empty view like this. And let's go ahead and assign empty view props. Let's extract the message and let's extract on new which is optional. Inside of here we're going to return empty which we just imported and we're going to give it a class name of border border dashed and background color of white. Then let's use empty header. So let me just quickly fix this component empty header empty media and variant of icon and inside of here render package open icon from lucid react. You can of course use whatever icon you think is best. I personally think this one kind of represents like an empty box. So now let's close the empty header and let's add empty title here, which will simply say no items like this and empty description. Let's go ahead and render the message. and we should only do that if the message is available. So again, render it conditionally. The reason I rendered these things conditionally is so they don't take any weird UI space and remember to turn it into a Boolean so you don't get any unexpected render results. Same thing for on new. If it exists inside of empty content here, add a button which you should already have imported here and give it on click on new and give it add item like this if you want you can also add props for like individual empty title label add new label I mean you absolutely can do that if you want to let's go ahead inside of workflows now and just as we've created workflows error and workflows loading and now let's create the workflows empty. Export const workflows empty. This one will be slightly different. So first let's go ahead and let's return a fragment and empty view inside of here. Just make sure you input that empty view here. And let's go ahead and pass in message. You haven't haven't created any workflows yet get started by creating your first workflow and let's go ahead and give it an on the new option by adding create workflow use create workflow because we already have it imported here actually from hooks use workflows and let's also make use of our premium model handle error and model from use upgrade model we also have this imported because we used both of them inside of workflows header and now very simply let's just add constant handle create which calls the create workflow mutates it, skips the first parameter because we don't have any options, but we need to pass something to access the second parameter, which has the custom on error option, which allows us to handle error and open the model if needed, which means that we also have to render the model just like that. And of course on new handle create There we go And now that we have workflows empty we have to render that as well The way we can do that is by actually not even leaving the workflows file. We can just go ahead and find workflows list here. And let's simply do if workflows.data.items.length is, I think this will work. We can just return workflows empty. Like this. So now, if you go ahead and search something obscure, you should see no items. You haven't created any workflows yet. Get started by creating your first workflow. So yeah, maybe not the best message to show for search empty, but you get the idea, right? This would appear if you had no workflows at all. So maybe you can change the message to something a bit more generic, like no workflows found, get started by creating a workflow. So it doesn't indicate like there are no workflows in this account. Maybe that's better. By creating a workflows, no workflows found. I don't know I like the longer message simply because it fills the space in a nicer way all right one problem though is that it gets it expands a lot but we're going to fix that very soon by creating our new component which is going to be the entity list so that's something that we actually don't have yet you can see how instead of my workflows list here I manually added this and I still render this inside of a completely centered JSON string. So now let's go ahead and let's focus on using the entity list. So for that, we go back inside of entity components where we just developed the empty view. And in here, we have to create an interface called entity list props. And I'm going to write this one together with you because it's a little bit more complicated. Interface entity list props accepts a generic. And then items will be a type of that generic. We're going to have a render item function, which will accept the item, which is the generic. And it will, of course, have an index. And it will return a React node. This will allow us to basically have our own dot map done within this reusable component. And in order to have a dot map, we also need to have a way to have a key for each object. So let's pass get key, which is technically optional. But, you know, it's a good developer practice to have this as an option to pass if for whatever reason we don't want to use the index as the key. And here we are going to pass an option for empty view. What if no items are found? And an optional class name if we ever wish to modify how the entity list looks like. Let's export function entity list and pass in the generic here. Go ahead and add items, render item, get key, empty view, and class name. and for the class name, we don't have to pass anything in the class name, actually. I mean, let's obviously add it, and let's assign entity list props and forward the generic from the entity list here. You're going to see how this generic will be used in a moment when we actually render it. So first things first, if items.length is equal to zero, and if we have the empty view component passed, in that case we're going to return a div and this div we're going to have a class name to center that empty list so flex1 flex justify center and items center and it's going to render the empty view within a limited maximum width so make sure to add this div here this way it will not expand like this it will kind of be smaller all right after this oh let me just expand a bit so you can see the code after this we have to actually render our items for that we're going to use a div and a class name and for this class name I'm going to use the cnutil so make sure to import cn from lib utils and I'm going to give it some default classes which will be flex, flex column and gap y4 and then I'm going to pass class name if we ever want to override that and then inside of here we can go ahead and iterate over our items. So each item will have an index accompanying it here and we're going to return a div like this and each div needs to have a key which will either be we have the get key function or we are going to manually call get key and pass in item index. This was an incorrect explanation. If we have the get key function, we're going to call the get key and pass the information of the item and the index. Otherwise, we're just going to use the index. And then let's use the render item function and pass in the item and the index. This way, we can easily change every component of this entity list to display whatever we want for either the render or for the empty state. Now we can head back to workflows.tsx inside of features, workflows, components, workflows. And now we can go inside of the workflows list and make this a little bit better. So I'm going to remove this entire thing and I'm just going to return the entity list from components entity components. And then let's go ahead and pass the items to be workflows.data.items.getKey is very simply going to get the individual workflow from each of this item list and it will use the workflow ID. And you can see how we have type safety here. And you're probably wondering how does it know exactly what the workflow is. Because of the items prop. Because instead of the entity list here, we decide that the items are the generic. So then we can reuse that generic type through all the other functions that we have and have type safety passed along. So if I change this to, I don't know, credentials or some other type, it will have that type here. So that's how this is working. That's why we needed that a little bit complicated interface to allow us to adapt to any items list that we pass. This is how people actually build those reusable UI components which render lists, if you ever wondered, like data grids and things like that. They need type safety like this, so it can accompany any scenario. Render item is the next prop which we need. And for now, let's go ahead and just get the workflow and render a paragraph which will say workflow.name. because we don't actually have a component. And empty view will simply be workflows empty. And now we should have a bit of a nicer experience. Here it is. And if I remove this, I should just get paragraphs of all of my workflows. And when I search for something obscure, I get my no items result. And when I click add item, all it should do is just create a new workflow. And you can see how this one does not redirect me there. So that's what I was talking about, right? Sometimes we might not want to redirect the user. This action definitely tells the user to kind of immediately go there. But you can decide for yourself. Do you want inside of your workflows empty here to also add an on success, get the data, and then do router, which you need to import from next navigation, push. workflows data ID if you want to you can do that and then every time they create a new one they will be redirected so perhaps that's a good idea to do here as well use router we already have user router imported perfect so now when you click add item from here it will also redirect there there we go perfect so let's go back inside of workflows now and now let's go ahead and render the workflow item component. So the entity item component, which we need to create now, will be a little bit larger, but it won't really be complicated in any way. So let's start by creating an interface entity item props. In here, let's start with a couple of easy ones. href title and subtitle, which is an optional React node. And then we will have optional image and actions. And both of them will be React.react node, also optional. Then we're going to have an optional on remove, which can be either a void or it can be a promise void. And then let's go ahead and add is removing and class name, which are all optional. One is a Boolean and one is a string. Now let's go ahead and export entity item with all of those props destructured. href title, subtitle, image actions, on remove, is removing, and class name. And inside of here, what we are going to do is we're going to return a link, which we already have. Make sure it's imported from next link. give it an href of href and prefetch option so automatically by clicking on the entity item you will redirect to its page what we have to do now is we have to import all of the card components i'm not sure if we have that so let's go ahead and quickly check in our import we don't so let's go ahead and add card, card content, description and title. You can also use this type of import since we are in the same folder. And we are also going to need all components from the drop down menu So same thing drop down menu content item and drop down menu trigger Now that we have that Let go back to our entity item and let start building the actual card here So card will have a following class name it will be dynamic so open cn we already have it imported by default padding for shadow none hover shadow and cursor pointer. If is removing we're going to make sure that it has some kind of opacity to indicate that something is happening and that it should not be interacted with because it's being removed and as the third we're going to pass the class name which can be whatever the user wants I mean the developer wants. So cart content will have a class name of flex, flex row, items center, justify between and padding zero. And then let's add a div with a class name flex items center and gap three. And in here, we're going to render an image. Then let's go ahead. So still inside of this div here, let's open a new div and a card title. And in here, we're just going to render the title of this item. The card title will have a class name of text base and font medium. And then we are going to have a check if the subtitle exists. In that case, let's add card description and render the subtitle. And the class name will be text extra small. And now we have to develop the drop down menu action. So let's go ahead outside of these two divs still inside of card content. And let's check if we have actions, or if we specifically have on remove. Let's go ahead and let's do div. Inside we can render the actions if the developers want to pass actions like this. Flex gap x4 items center suggests some simple styling here. And now let's specifically develop on remove here because this will be the one most commonly used in our case. We open the drop down menu. We open the drop down menu trigger. We make this trigger behave as child so it becomes the child element which in our case will be the button. This way it won't be rendered as button within a button but it will use the styles of our chat cn button. That's why we are doing so. Size will be icon. Variant will be ghost. On click will be event. Event stop propagation. The reason we need this is so that when you click on the drop down menu you don't trigger the link which redirects the user away. So that's why we are stopping propagation here. And inside we're going to render more vertical icon and let's go ahead and give it a class name size 4. Make sure you've imported this from Lucid React. Outside of the trigger, let's go ahead and add drop down menu content and it needs to have a few props as well. First, align which will be end and on click which will again stop the propagation. So even when you click in the drop-down content, it doesn't count as a link click. And finally, drop-down menu item and trash icon from Lucid React with delete text and a class name to the icon of size 4. Again, make sure you've imported trash icon from Lucid React. And now we have to develop the handle remove method, which will be very simple. so it's just going to be a generic we already have on remove here so what we're going to do is cont handle remove make it asynchronous event will be react.mouse event first things first prevent default then stop propagation check if you are already removing and then uh i'm my apologies check if we have the on remove method and then await on remove and you could perhaps check if is removing just break the method this way you cannot trigger it twice and then very simply on the drop down menu item add on click handle remove there we go now we have the entity item and now that we have that we can go back inside of workflows.tsx. And let's go ahead and let's develop the workflow item. So the way we're going to do that, let's just do export const workflow item. And the type will be workflow. and you will be able to import the workflow type. Let me see, do we maybe have it already or do we not? We don't have it. I think we can import it from Prisma schema. So let's import workflow from generated Prisma like this workflow and let's import it as a type workflow. So it's referring to our schema model workflow. This way we have its exact types. And then we're going to map that down here. So that's exactly what it will be used for. And let's destructure the, let me see, should it be data? Okay, we're going to pass it as data workflow like this. so we don't individually have to pass all the fields. And in here, let's simply return entity item from components, entity components. Make sure you've added this new import. href is going to be forward slash workflows forward slash data dot id. Title is going to be data dot name. Subtitle is going to have a fragment here updated and let's go ahead say to do then we're going to have an empty space add bull as in bullet point created empty space to do so makes no sense now but we will fix this in a second now for the image let's go ahead and for now i think we can just pass an element like div class name size 8 flex items center justify center and the render workflow icon from lucid react inside here's the tricky part again lucid exports both workflow and workflow icon both of these are valid but make sure you don't use the workflow from lucid react because it will conflict with the type here so just make sure to use the workflow icon honestly either also do this as workflow type maybe but it's okay for now if we don't have any other workflow thingy named inside of this file so workflow icon is rendered here now let's go ahead and give this a class name size five and text muted foreground later this image will be completely dynamic and depending on the first trigger of the workflow, it will display the image of that trigger. For example, Stripe or Google Form or Webhook, like things like that. But since we don't have that developed at all yet, we're just going to fall back to this simple workflow icon. All right, now that we have that, let's go ahead and just pass in on remove to be an empty arrow function is removing to be false. And I think that now we should be able to revisit our workflows list component. Here it is. So inside of the render item, now instead of rendering this paragraph, let's render workflow item. and pass in data to be workflow. And let's see, this now isn't compatible, it seems. So user ID, string, user ID. Let me see. Created at are not compatible. Ah, I think I know exactly why this is happening. It should work. If you take a look, it looks fine, right? But why are the types wrong? well as you can see here yeah things happen during the drpc to server component to hydration to cache types get lost especially for the kinds of types which are in the database referred to as a date but on the front end are referred to as a string for example types of property created at are incompatible. String is not assignable to date. So yeah, that's a problem because one has a, I guess, a timestamp of some sort in a string and the other one has an actual date, right? So how do we fix that? We fix that with SuperJSON. So let's go ahead and do npm install SuperJSON. And I will show you the exact version I have. SuperJSON. Usually I teach people to install SuperJSON during the TRPC installation, but this actually might be better because you actually learn why we need it. So you can see, yes, now we have these problematic types, right? Incompatible with our frontend and our database, because database has, we're using Postgres as the database, and we are basically using schema to communicate to Postgres using those types, right? But our JavaScript app on the front end doesn't have the same types as our Postgres.\ndatabase. So we need some way to transform that data to make it compatible and to make it, you know, able to communicate both ways from the database to the front end and from the front end to an API call, which needs to create something in the database. And the way you fix this is super easy. You just have to revisit our trpc init file here and actually all of the files. So let's see. in here we already have inside of init trpc.create you can see we have data transformers and you can actually learn more about them by following this link super json allows us to transparently use standard date map set over the wire between server and the client basically that was the problem you can return any of these types from your ap resolver and use them in client without having to recreate the objects from JSON. So that's how that works. So we can now finally enable the transformer super JSON and we can import super JSON. Did I show you my super JSON version? I don't think it matters that much, but it's 2.2.2. So super JSON, as you can see now, is added as a transformer here. And now I think there are some other places where we need to add it. Let's check the server. itself okay i think not needed here query client in here we need it so you can see it's already commented out so import super json from super json and instead of make query client uncomment serialized data and uncomment deserialized data this way it will work both ways on the database and to the database and to the client. And client.tsx, let me check if we have something here. We do and we also have an error unless it is enabled. So make sure to uncomment transformer inside of HTTP batch link in client.tsx. And let's also just import superJSON from superJSON. And once you do that, I think that's all. I think we so we added it inside of client we added it inside of init we added it inside of query client the only place we didn't add it is inside of server and I think we don't need to add it here if we've done this correctly and now revisit workflows.tsx there should be no error you can see that updated at and created at are both treated as date if you are still getting errors you can just reload your window or restart your typescript session or restart visual studio code so that's what super json dot it does it's a data transformer yes i'm actually happy this happened because this way you can learn why we need it rather than just then just blindly follow me with installing it and we already saw that the ui is working just fine so nothing much to really view here. It worked before as well, but now the types are correct as well. So later, and yes, clicking on it redirects to individual workflow. Later, each of these little icons will change depending on what is the first trigger node in each workflow. So if it is like a Stripe webhook, it will change to a Stripe icon. If it's a Google Form, it will change to Google Form. If it's manual execution, it will have a little arrow, things like that, little details which are going to make this look better. Now let's fix the to-do thing and let's enable the deletion. So fixing to-do is quite simple. Let's go ahead and npm install date fns. As always, I'm going to show you the version that I installed simply in case you're wondering, oh, why don't I have the same API? It could be that you have a completely different major version depending on when you're watching the video. All right, now that we have this, let's go ahead and import a couple of things from date FNS. So let's import actually just one thing, format distance to now. That's the one we need. And let's scroll all the way down to our workflow item. And let's change the to do here to instead be format distance to now data.updatedAt because that will be our primary order, right? Which was the most recently modified workflow to show first. So we are showing it first here as well like this and created also format distance to now data createdAt. and this way you will see updated 21 minutes, created 21 minutes. We currently don't have an option to update, so all of them will be exactly the same and if you want to, you can also add to both of these an option to add suffix. Add suffix true like this and when you add that to both of them, it will have the ago. So updated 21 minutes ago, created 21 minutes ago. Perfect. One more thing left to do. And that is the on remove method. So let's start with our, okay, I just closed everything. I didn't need to close. Let me go inside of routers.ts here. Do we have the remove method? Let me check. we have. It's had protected procedure. Great. And it will delete a single unique file using the unique field and using permission user ID field. Perfect. But what I think we don't have is use workflows hook for it. So we have it to create workflow. We'd never developed one to remove workflow. So let's do that first. So hook to remove a workflow export const use remove workflow. We're going to start usual by adding trpc and use query client. I think we have both of them imported here already and let's return use mutation. In the first argument we are passing trpc.workflows.remove and we are opening the mutation options. Instead of the mutation options we are passing on success and if it succeeds let's go ahead and first do toast.success and let's go ahead and indicate which workflow was removed. Workflow data.name removed. If you want to you can put it in quotes. Let me see what how do the other toasts look like. The other put it in quotes so it would make sense for this one to do that as well what i like to do here now is i like to invalidate my queries trpc workflows get many dot query options like this it's also perfectly valid to not want to invalidate all queries and instead just want to manually go inside of cache and remove that one that's also i mean that's obviously a better solution but the thing is we had limits in place so when you reinvalidate it's not like you're going to refetch your entire workflow database you will just refetch at max five or ten items for that user right i find it easier especially in tutorials you know to just invalidate the entire queries same thing goes when you create a new one it's easier for me to just invalidate and load new ones rather than go inside of workflows cache and then append one to the top or here filter out by the id but you can do that yeah that would technically be even better but you don't have to worry about this being super unoptimized because we do have limits in place inside of our constants here so we don't let just a billion records to be load every time we invalidate perfect so now that we have use remove method here we can go inside of workflows.tsx and instead of workflow item we can actually use it so what I'm going to do is I'm going to do the following I'm going to import mutate async I'm going to extract mutate async and I'm going to map it to remove workflow and I'm going to extract is pending and let me go ahead and use import use remove workflow here from all of the other ones and actually I don't need to complicate this that much but I can just do remove workflow no reason to complicate it const handle remove like this. Let's go ahead and just do remove workflow.mutate. And in here pass in id data id. And then inside of on remove we can pass in handle remove. Remove workflow is pending. There we go. So if we did this correctly I will try to remove harsh breezy portugal by clicking delete you can see it's grayed out and it's reinvalidated and removed amazing job so we just finished the entire ui for our handling workflows api thingy and what's cool about it is that we now have a set of components that we can reuse for credentials and for executions so we no longer have to worry about building any of that from scratch we can easily reuse it for anything. We can reuse the pagination, we can reuse the header, new buttons, everything. We can reuse the empty states in here, we can reuse the loading states. Let me try and demonstrate it. No luck. Oh, okay, you saw it. Perfect. We can reuse the errors and the delete methods here as well. Perfect. I believe that marks the end of this chapter. So let me see. we created the UI components for loading error empty list and for item and now let's go ahead and push this to github so 13 workflows UI let me first review my files here nine files package json page entity component workflows use workflows client init and query client where we initialized super json so I going to go ahead and open a new branch here 13 workflows UI then I going to stage all of my changes and I going to make a commit 13 workflows UI I going to commit and I going to publish the branch perfect and now let's go ahead and do our github pull request review so compare pull request create pull request and let's have another set of eyes take a look at our code. And here we have the summary. We enhanced workflows page with dedicated loading and error views. We added an empty state with a guided flow to create the new workflow. We introduced a consistent workflow list item with images and human readable timestamps. We enabled deleting workflows with user feedback on success. And of course, we integrated SuperJSON for more reliable data handling and compatibility across the app and added SuperJSON as a runtime dependency. Exactly. In here, as always, file by file walkthrough. There wasn't too much business logic to review here really, but perhaps some of you might be interested in this, which is the diagram explaining how SuperJSON serialization and deserialization happens. So feel free to pause the video and take a look here. Again, great catch by CodeRavit explaining what seems a super simple thing we did, which was actually very important for this project and to make it work all together. And good thing is this project didn't include too much business logic. So we actually have only one comment to add error handling for workflow removal. So we have on success, but we forgot to do on error. So yeah, definitely something we could add. Other than that, no comments. Let's go ahead and merge this pull request. And after we have merged it, let's go ahead back inside of our main branch. Let's hit synchronize changes. And as always, I like to confirm once the changes are synchronized inside of my graph here. 13, workflows UI, merged back here in the main branch. I believe that marks the end of this chapter. Let me go ahead and mark it as finished, pushed to GitHub, created a new branch, created APR, and reviewed and merged. Amazing, amazing job, and see you in the next chapter. In this chapter, we're going to focus on creating the individual workflow page. By using the workflow ID, we will be able to prefetch on the server component, use suspense query in the client component, and display loading and error states with our suspense and error boundary components. We are then going to create a useful workflow header component, which we are going to use to display and update workflow name. And we're going to end the chapter by creating the editor component, which will display react flow, where we are going to finally start adding some nodes. Let's start by doing the prefetch use suspense query loading and error because we have just done that for the entire workflows list page. One thing I want to bring your attention to is that when we initialized this project here we used biome as our linter. But you've probably noticed that during this entire tutorial I never really got any biome warnings. That is because I never installed the biome extension. So if you actually want to see the benefit of using biome you should probably use the extension. But keep in mind that biome is pretty strict. For example, if I go inside of source trpc server.tsx, you can see that now I have all kinds of warnings and infos here. For example, biome is telling me that I need to sort my imports and exports. It's also telling me that I should make sure to only import things which are types separately as types. It's also telling me not to use any. Now, these warnings shouldn't really break my build. The only thing that can break the build are TypeScript errors. So I do know that there is an option to fix all of this at once by using a formatter. and you can set that formatter to apply every time you save the file or you can manually format your document and then configure the default formatter to be biome and you can see how that kind of fixes the issue but in this tutorial I'm actually not going to do that and the reason why is I don't feel comfortable with you not seeing exactly every single line that I change because if I use the formatter, sure, we can see that it modified some files here, but I'm pretty sure you didn't notice that it also modified the files down there. And I don't want to leave you in a situation where you have different code than I do. For that reason, and for that reason only, because this is a tutorial, I will not be using the Biome extension at all. If you want to, you can, of course, use it. Linters are overall a good thing and if you use the save format on save it will be obviously a better quality code but just for tutorial purposes to make this easier for you to follow I will not be using Biome at all. I still recommend using Biome. I did use it while I was developing to catch some issues but for tutorial sake I'm not going to use it because it has a bit too strict rules that I will waste my time on. So let's go ahead now and let's focus on doing load workflow page by ID. And yes, you can always format your entire project when you finish it. So just like format all files and fix them. But keep in mind that almost all files from ChatC and UI will be broken according to biome. So it will be quite a lot of changes. So decide for yourself maybe while following this tutorial don't fix every single biome issue you will just spend a lot of time doing that maybe later when you finish the project all right now finally i can go into building this so i want to start by seeing do i even have my features workflows server routers.ts. Do I have get one? Looks like I do. It is protected and it is queried by ID. Perfect. And it's fetching for this user permission. Great. Exactly what we need. So now what we should do here inside of workflows, let's go inside of prefetch here and let's create a prefetch for individual workflow. So that's actually quite simple. Prefetch a single workflow. Export const prefetch workflow. Accept an ID which is a type of string and just as above prefetch drpc workflows this time using get1 and query options are just that single ID and save the file. Now we have prefetch workflow. Now let's also do the equivalent in the hooks. Instead of use workflows here, we have use remove workflow, we have use create workflow, and use suspense workflows. So down here at the bottom, I'm going to add use suspense workflow, a hook to fetch a single workflow using suspense. So use suspense workflow also accepts an ID, initializes trpc and simply returns use a suspense query trpc workflows get one query options and passes in the id this is how it looks like in one line there we go let's go ahead and continue with this for now once we have those two we can go ahead inside of app folder dashboard editor or workflows workflow ID page.tsx and we already have something prepared here so we are protecting this page great and we are even extracting the workflow ID from the params which means that if I go to localhost 3000 and just click on a random workflow I should be seeing the workflow ID here and I do which means that this is working as expected and since I do that it also means that I can now safely prefetch here. So prefetch workflow, not workflows, just workflow and pass in workflow ID. Make sure you have imported prefetch workflow. A quick reminder, you do not need to await this. So the prefetch, which we have defined in server.tsx is using void for that specific reason. It's not returning any value. It is simply populating the cache. So now that we have prefetched this let's go ahead and let's save some time by going inside of arrest workflows page.tsx and let's copy hydrate client and suspense and error boundary like this paste all of them here let's go ahead and close error boundary and let's close hydrate client Now let's go ahead and indent these things. There we go. Let's import Hydrate client from the RPC server. Let's import error boundary from React error boundary. Let's import suspense from React. All of these components. Great. now let's go ahead and temporarily just set the error boundary to be error in a paragraph and let's go ahead and set the loading to be a similar solution like this now let's go ahead and create the editor component so inside of our features folder I want to create a new folder called editor and in here I going to create a folder called components and the file editor Let's mark it as use client and let's import use suspense workflow from features workflow hooks. And let's export const editor. Let's go ahead and give it a simple prop type. workflow ID, which is a type of string, workflow ID. And then in here, const data, remap it to workflow, use suspense workflow and pass in the workflow ID. And let's go ahead and return a paragraph. Make sure to close it. JSON stringify workflow and some settings to format that stringify in a nicer way. Make sure you added use client at the top. Now let's also make sure to add editor loading and editor error. So for this we can reuse entity loading view from components entity components. So just make sure to return it and the message here will be loading editor like this and export const editor error return error view from same and message will be error loading editor so from entity components perfect so now that we have those two we can go ahead and go back inside of the page we can replace the error with editor error and we can replace the suspense with editor loading and inside of suspense we can render the editor from features editor components editor and make sure to pass the workflow id which is workflow id there we go Now this will be prefetched and it will be loaded or it will be thrown if there is an error. Let's quickly take a look. Looks amazing. There we go. Now that we have that, you've probably noticed that we are missing a header. For example, my workflows has a header, which I can use to collapse the sidebar. But if I go inside of an individual workflow, that header no longer exists. That's because as explained here, we're going to have a workflow header, which will have the ability to update workflow name. Since it has that complex feature, I made the architectural decision to separate it in its entire organizational folder here. So let's go ahead and work on that workflow header now. The first thing we have to do is go inside of source app folder dashboard editor workflow ID page and wrap the children inside of suspense inside of a main like this and give it a class name of flex one and above that main create editor header render which doesn't exist yet we're going to create it in a moment and pass in the workflow ID like this. The reason we are doing this exact structure is because we are replicating what other routes have in their layout. You can see it's exactly that. The children are wrapped in main and above main is the app header. We're doing that exact thing but directly inside of page.tsx because we don't really need the layout and also because instead of editor header we're also going to use use suspense workflow. So because of that we have to wrap it instead of suspense and error boundary and hydrate client and I think it makes more sense to do that inside of this page than to purposely use a layout file because technically there is no need for a layout in a single route folder. So let's focus on creating the editor header component. We can speed things up by copying the existing app header component. So let me just copy this entire file content. and let me go inside of features, editor, components and create a new file called editor header.tsx and paste the content here. Let's rename this to editor header and let's modify the props so that they accept workflow ID. There we go. let me just extract the workflow id perfect and let's go ahead and go inside of our page dot tsx and let's import editor header so so far we've imported the editor component editor editor error and editor loading and we've now added editor header from components editor header You can see that now I have that same collapse button. Now it doesn't make too much sense why we did this because it looks identical to the one in the list. But now we're going to extend it by adding breadcrumbs so we can see exactly what workflow we are on and the ability to modify it. So after sidebar trigger, let's create a div and a class name. Make sure you're doing this inside of the editor header file. Don't accidentally overwrite your app header. You can close it. Let's give this a class name of flex, flex row, items center, justify between, gap x4, and full width. Inside, let's first create editor breadcrumbs. And pass in the workflow ID as the prop. And then let's do editor save button. And you can copy the prop from above. So editor, the breadcrumbs and editor save button. Let's go ahead and develop them one by one. So editor save button, and we can copy the exact thing like this. Now the editor save button will just return a div with a class name, ML auto to push it all the way to the right side by giving it a margin on the left side and it will render the button from components UI button save icon from lucid react with a class name size4 and a text which says save size will be small on click for now will be empty and disabled will be explicitly false. So we remember to modify it later. All right, that's one down. Now let's copy editor save button and let's go ahead and create editor breadcrumbs component. And once you save and refresh, the errors should no longer appear. Let me see why they are appearing here uh-huh okay it is because we forgot to add use client to the editor header so use client and once we do that and refresh i'm very confident we should no longer have any errors we should just have two instances of save buttons it's obviously because we didn't change the content of the editor breadcrumbs so let's go ahead and do that in order to do that we are going to have to import all the components from breadcrump. We already have it when we added chat CNUI. Breadcrump, item, link, list, and separator. And while we are here, let's also import input. And let's also import useEffect, useRef, useState from React. And I think for now this is okay. Let's just add link from next link. We will add some more imports later. Let's for now focus on making the bread crumbs look like something. So instead of returning a div, it will return breadcrumb. so let's go ahead and close it inside it will return breadcrumb list then breadcrumb item then breadcrumb link give it an as child property so it becomes the next js link which we separately imported and it doesn't use because otherwise if you don't do it it will just use the normal anchor. So we can now prefetch and we can just redirect user to workflows. And this will say workflows. Great. Outside of the item, add the breadcrumb separator. And then in here, just leave it empty. And you will see how this looks now. So in here, obviously, we are going to render the individual workflow name. But when we click on the workflows, we get redirected back to workflows. Right? We have this smooth navigation here. Now what this will allow us to do is that when we click on what's going to be the name of the workflow, it's going to switch to an input and we will be able to change the name. In order to save the name, we have to make sure we have two things ready. Let's go inside of routers.ts, inside of workflows, server, routers.ts, and let's find update name. Just make sure you have it. The only thing we should accept is the name and the ID. It should be a protected procedure, and in the where clause, you make sure that you query by user ID as well as the ID. Perfect. once you have that let go ahead inside of workflows hooks use workflows so we have used remove workflow we have used create workflow let go ahead and copy use create workflow And let go ahead and paste it here And let's rename it to use update workflow name. So hook to update a workflow name. We need query client and trpc. Instead of calling workflows.create, we are calling update name. On success, it will be workflow data name updated. We are going to refetch this and we are also going to query client invalidate queries. trpc workflows get one query options id data dot id. This way the individual one will be refetched as well. So it has a new name. And this will be failed to update workflow error message. Great. Now that we have used update workflow name, let's go back instead of the editor header. And let's go ahead and let's export const editor name editor. Or maybe editor name inputs. Maybe that one is better so we don't repeat the word editor so much. so let's repeat this type here you can copy of course and in here we're first going to get the data remap it to workflow from use suspense workflow and pass in the workflow id now don't worry just because inside of page.tsx here we render both the editor header which now uses use suspense workflow and the editor which uses use suspense workflow it doesn't mean that there will be two requests it just means that one of them will be cached so yes that's the cool thing about using react query and let's go ahead and let's do update workflow name or maybe we can just call it update workflow use update workflow name perfect now let's go ahead and let's return breadcrumb item and render workflow dot name it says that workflow is possibly null let me check how that is exactly possible because we are using use suspense workflow. So that should not happen. Let me check inside of page here. Use suspense workflow. We are returning use suspense query. This should guarantee that the workflow exists. I think it might be due to the way instead of use suspense workflow and specifically instead of the get one here. Yes, because of this. Let's do it like this instead. Const workflow is await Prisma workflow. Turn the query into an asynchronous component and if there is no workflow let's throw new trpc error instead because usually this will not throw or maybe I can use find or throw my apologies maybe that's easier find unique or throw exactly if I use this I think that then my header here will always have this yes so we have to throw the error if it didn't find one There's no point in returning null because we don't handle that, right? We can't do anything with something that's null. And now we no longer have that issue. So just make sure you have modified this get1 to find unique or throw. I will go through the rest of the code to see if there is an opportunity to fix that anywhere else here. For example, delete, no, it's just delete. create is just create okay I think there are no other places to check other than this but yes the get one needs find unique or throw because we will display an error if it doesn't exist and now that we have a update workflow here we just have it ready we have this workflow name we have editor name input and now in here after the separator let's render editor name input and now also pass in the workflow ID. Here it is. And now you will see the name of your workflow. So if you go ahead and change to this one, Breezy Early Pencil, you will see the name Breezy Early Pencil right here. Now let's go ahead and make it so that it visually appears clickable. We're going to do that using CSS. So let's go ahead and give the breadcrumb item a class name of cursor pointer, hover text foreground and transition colors. And now when you hover over this, you will see that you can visually click on it, right? And when you click on it, something should happen. What will happen is that we're going to switch a state from is editing from false to true. so is editing from use state give it a default value of false is editing and set is editing and let's prepare name set name use state the default value is workflow dot name which will always be some form of data because otherwise we throw an error we fixed that just a moment ago and let's prepare the input ref so we can always focus on this when we switch input element as the type HTML input element as the type and let's go ahead and do use effect like this. If we have workflow.name, set name to workflow.name. This will update if any new name is received which will happen when we revalidate. and another use effect here, which will do the following. If is editing and if input ref dot current, in that case, let's go ahead and do input ref dot current, focus, and let's also do select. And in the dependency array, pass in is editing. You don't have to pass the ref. Let's develop handle save method. And in here, if name is equal to workflow.name, what we are going to do is set is editing to false and break the method. We didn't modify the name. Otherwise, let's go ahead and let's do update workflow. So what we can do is we can actually open try and cache here. So turn this into an asynchronous. and instead of try do await update workflow dot mutate asynchronous passing the id to be workflow id and the name like this if that fails make sure to reset the name and then finally so regardless if it fails or succeeds make sure to reset set is editing back to false this way we handled most of the use cases that can happen. And let's add one more method, const handle key down, which accepts the value of react.keyboard event. If event.key is enter, handle, save. Else if event.key is escape, set name to whatever was the current value, and reset the editing status. So just some hotkeys for easier editing. And finally, if is editing, instead of returning breadcrumb item, we are going to return an input component. It will have a ref of input ref. it will have value of name on change which accepts the event and passes it to set name as event target value on blur automatically triggers handle save if you don't like that you can of course remove it on key down handle key down this is a listener for hotkeys and class name height 7 with auto minimum width of a hundred pixels and px of 2 and now it should be ready right now it's called breezy early pencil and let me go ahead and yes of course one thing we forgot was to actually add on click to the breadcrumb item so on click set is editing to true so let's go ahead and try it now when I try to click here there we go it automatically selects if I click escape nothing happens but if I change from pencil to rename and press enter and did it it did workflow breezy early name updated and it should also change it here in the data right so if I refresh it should fully stay renamed I just don't like that when I pressed enter it didn't immediately close so when we press enter handle save happens perhaps we should immediately do set is editing to false set is editing to false like this and maybe remove the finally and you know maybe you can improve it by not allowing to change back to this mode if it's pending so test two immediately closes and then you can see it renames maybe that's a little bit of a better experience uh and you can also do disabled if let's go ahead and do create things\nupdate workflow dot is pending and then maybe you don't have to do it here maybe you can then revert to finally just experimenting now so click here remove something yeah it's kind of disabled okay yeah that looks fine that's good i like this solution perfect so you can see now it's renamed to breezy early. Let's confirm by going back to workflows. You can see updated less than a minute ago, but created two days ago. So obviously works. Great. So there's one thing left inside of the editor header, and that is the save button. But we can't really do anything here. We will develop this later because we the save button will be used to save the current workflow editor state, the nodes, the connections, executions, those kinds of things. This is fairly simple developer experience. We can just save on blur, but the save button will somehow need to have the state from the editor. So I know that I've said that I want to add workflow in this chapter, but it kind of makes no sense because we are already half an hour here and I'm fairly satisfied with what we did here so I want to call it a day here and skip this part for now and instead dedicate the next chapter to it so I can properly explain it and go a bit more in depth so let's wrap this up so 14 workflow page I'm gonna go ahead and create a new branch 14 workflow page and then i'm going to go ahead and make sure to commit all six changes so stage all changes 14 workflow page commit and publish branch once it's been published as always let's go ahead and let's review the pull request so i'm creating a new pull request and now we're going to review it. And here we have the review by CodeRabbit. New features. We introduced a full page workflow editor with header, breadcrumbs and inline name editing. We added save action in the editor header with clear disabled state during saving. We provided loading and error reviews for smoother editing experience. And we improved handling for missing workflows. Users now see a clear error when a workflow doesn't exist. This is referring to our find unique or throw change in Prisma. As always file by file summary here and as per the sequence diagrams this isn't really anything we haven't seen before. This specific one is referring to how prefetch is working and what happens in the seed cache here. I'm pretty sure you understand this by now since we did have a couple of examples of this already but if you want to take a look at it you can of course pause the video. I will you know slowly go over it here so you can see exactly what is going on. Basically prefetch seeds the cache and then our client component, our UI component immediately has access because it hits the cache. And it's pretty clear how our editor name input works as well. We actually have no actionable comments. There are some nitpick comments. So for that reason I told it to summarize what we've learned here and yes CodeRabbit can do that as well. You can talk to it like you would normally to a actual person. And what I think is important here is this. We changed from find unique to find unique or throw in the router to explicitly handle missing records paired with try catch blocks in the UI that revert optimistic updates on failure. Exactly what we did and I'm pretty sure we knew all the other ones already. Overall pretty good pull request not much to comment on it was pretty simple amazing job. After you've merged that go back inside of your main branch and make sure to synchronize your changes and once you've done that as always I like to end by going inside of graph and making sure that I can see my newest merge right here excellent and let's go ahead now and wrap it all up by marking the github part as finished amazing amazing job and see you in the next chapter where we are going to start to implement the editor using React Flow. In this chapter, we are going to develop the editor component. By adding React Flow dependency, we will be able to drag nodes around within a canvas and establish connections between them. We are then also going to update our Prisma schema by adding a node and connection table. This will allow us to preserve whatever we create within the editor canvas such that we will be able to store it in the database. And once we have that established, we will be able to end the chapter by loading the default editor state from the database. Let's go ahead and start by adding React Flow into our project. Using the link on the screen, you can visit their landing page. And clicking on Quick Start, you can visit the documentation. Since we already have a project, we can just follow the second step, which installs xyflow react package. So I'm going to go ahead and install this package. And once it's been installed, I'm going to show you the exact version that I am using. Here it is, 12.8.6. You don't have to use the exact version, but I just want you to be aware which one I'm working with in case something doesn't work for you. So now let's go ahead and make sure that we have our app running. And once we have our app running, let's go ahead and go into an individual workflow page. Right now, the only thing that should be visible there is a stringified JSON of the workflow data. So if I click here in Breezy early after I load the editor, I should only see this. And now we're going to follow the usage here and modify our editor.tsx. So editor.tsx. It is inside of features editor components. Let's start by adding use state and use callback from React. And then let's import all of these from our newly installed xyflow react. React flow, apply node changes, apply edge changes, and add edge. Then let's also import XY flow styles. And I like to do that here at the end after all other imports. Then let's go ahead and establish the initial nodes. We're going to do that above the editor constant since that's the one we are interested in. So as you can see the initial nodes will be two objects. Both of them will have their position coordinates and their data object which establishes their label. So both nodes have an ID, position and data. Now that we have the initial nodes established let's also add the initial edges. The initial edges will be a very simple array with a single object inside. And that object will have a source referring to the ID of the first node that we have here and the target referring to the second node that we have here. So make sure that source N1 matches the ID N1 and target N2 matches the ID N2. and we also give the edge an id simply because every element needs to have its own id and for the position it's pretty self-explanatory but node 1 is strictly in the center whereas node 2 will be initialized slightly below on the y-axis so now that we have the initial edges Let's go ahead and set up the state here. So after useSuspenseWorkflow, let me go ahead and collapse this. There we go. So after useSuspenseWorkflow, make sure to add nodes and edges as well as their setters using useState and set the default value respectively for nodes and for edges to the constants we defined above. Now what I like to do here is immediately create the types for this. So this will be a type of node which you can import from xyflow-react and it's going to be an array of nodes. And this will be a type of edge. Again, you can import both node and edge from xyflow-react and you can even specifically say that this is just a type. So now you will have the exact types in here. When you hover over nodes, you can see it will become node. Now let's go ahead and let's add these three methods. On nodes change, on edges change, and on connect. All of them are exactly the same as you can see. Let me go ahead and expand this as much as I can so it's easy for you to look at. use callback receives new changes and immediately returns and calls set nodes. Inside of set nodes, we can open a callback if we want to receive the snapshot or basically, in other words, the current value of nodes. So technically, you could also just call apply node changes and instead of using node snapshots, you could just pass nodes, but you shouldn't really do that. If you want to get the most update value of the state, you should use the callback parameter like this So I will zoom out just once more so you can see how all of this looks in one line because it a long line and I don want you to be confused so use callback receives changes and immediately calls set nodes set nodes calls a callback and immediately calls apply node changes and all of the other ones are the same except they are using set edges and call apply edge changes in on edges change or add edge in on connect. All of these apply node changes, apply edge changes and add edge can be found in the import above. Now in order to fix the changes type we also have to specify them here. So this would be a type of node change and then an array. You can also import the node change type. Let me just go ahead and specify it from here. So type node change and let's also go ahead and prepare the type edge change and type connection. Now that we have those let's go ahead and call this edge change and an array and this a connection. There we go. Now we no longer have any type errors here as you can see. So once we've established these three functions we can finally render React flow. So let me clean up my return here. We're going to create a div with a class name of size full and then we're going to render react flow now let's go ahead and pass in the nodes edges and those three methods we've just created nodes edges on nodes change on edge change and on connect and let's add fit view fit view will automatically zoom in on the nodes that we have and now as you can see when you refresh in an individual workflow id you will be able to drag these nodes around and they will already be connected the reason they are connected is because we established the initial edges right so initial edges show that node 1 is connected to target node 2. Perfect. Now let's go ahead and learn how to improve the React flow look. So if you change this from a self-closed component to this one and add a component called the background which you can also import from xyflowreact and save you can see that now we get a background grid which is already looking much better. Let's enhance it even further by adding controls. Again, from xyflowreact. And now in this corner, you will be able to zoom out, zoom in, or fit view. Let's also add minimap. Again, from xyflowreact. And now in here, you will be able to see a minimap of what's going on. And since I know some of you will be asking me this, down here you can see the text react flow and i think they fully deserve to have that text here and i'm going to show you exactly how you can support them as a company if you have the resources or if your company does do consider supporting react flow package this is as far as i'm aware the only way they maintain this and React Flow is open source and MIT licensed software which means that you are allowed to use it without supporting them but as you can see with your subscriptions you are ensuring the sustainable maintenance and development of the React Flow library. The reason I'm telling you this is because now I'm going to show you how you can remove their logo if you want to. So you can add pro options here, hide attribution set to true. I would recommend that if you do this, do consider supporting them because if you don't do it, the minimum they deserve is recognition. And you can see that when you add pro options, you hide their attribution here. And when you don't have it, you have a little text React Flow. Again, it's not required, but I think it would be very nice to support open source MIT license projects like this one. They are absolutely amazing. Great. Now that we have that established, let's see what's next on our list here. So we just created the editor component. we added the React flow and the initial nodes. Now let's go ahead and update the schema and add the node and the connection table. In order to do that, we have to go inside of Prisma schema. So now I'm going to expand this so we can focus exclusively on that. So below the workflow table, create a model called node. Now you can copy the type of ID because it's going to be exactly the same and now you have to create a relation with a workflow. So each node will have a workflow ID which is a type of string and now we have to create a workflow type of relation. So workflow relation is referring to field workflow ID and it is referencing the ID field in the workflow model and when we delete the workflow let's also delete this node. We can ensure that happens by adding onDeleteCascade. And once you've established that, you also have to go back inside of the node, inside of the workflow here, and add the nodes relation like this, node. And then you will have no errors. Perfect. Now let's go ahead and give this a name. Let's go ahead and give this a type. Now the type will be a specific custom node type. So this does not exist yet. We are now going to create it. So I'm going to go here and I'm going to do Enum node type. For now, let's go ahead and create initial as that's the only one we will be working with. So now that we have the type node type here, let's also add position, which will be a type of JSON, data, which will be a type of JSON with the default of a stringified object like this. Then let's go ahead and copy created at and updated at timestamps here. and let's go ahead and see. I think this is enough. I think that's all we need right now. And now let's also create the connection model. Model connection will represent the edges. So model connection, go ahead and you can copy these three lines here. so each will have an id and workflow which means that we have to go back to workflow and add connection connection and change this to connections like this inside of connection here let's do from node id and make it a type of string from node make it a type of node relation called from node fields from node ID references ID on delete cascade. So if the nodes get deleted, we can also remove this connection. And now let's go ahead and let's copy these two and change this to be from node ID to be to node ID. So from what node to what node? and change this to be toNode. Change the name to be toNode, and change the toNode ID to be used as the field here, like this. Let's also add fromOutput to be a type of string, default, main, toInput type of string, default, main. This might come in handy. I'm going to see if we're actually going to need this or not, but let's leave it here for now. And let's copy the timestamps as usual. And let's go ahead and set the unique field here to be from node ID to node ID from output and to input. Once we have that, let's go ahead and create the relations back in the node here. Output connections will be connection with a relation name from node. Input connections will be a connection, a connection array. Let me fix the typo. With a relation called toNode. There we go. Once we have that, I think that we are ready to create a migration. So we've created the node. We've created the initial node type and connection. Perfect. So let me go ahead now and do npxprisma migrate dev and let's call this react flow tables. So react flow tables and once we do that we have applied the migration. I now recommend restarting next.js so everything works as expected. Perfect. So now that we have that, what we can do and what we should do is go inside of source features workflows server routers.ts. And let's specifically focus on the create. Now what we are going to add is that every time a new workflow is created, let's also create a set of nodes. So nodes create type will be node type which we can now import from generated Prisma dot initial position let go ahead and make x 0 y 0 and name node type dot initial We're going to see if this name field is actually useful or not. I think that most of the time we're just going to use the type. So maybe the name won't be all that useful. For now, add it here so you don't get any errors. and now every time you create a new workflow you will also create a new initial node in that workflow. The problem is in Editor TSX we don't ever use that right because first we don't even include nodes you can see our workflow type doesn't even have any nodes here so we can't pass them here. So what we have to do while we are here is revisit our get1 protected procedure so let's go ahead and start by doing const workflow to be await prisma workflow find unique or throw make the query async like this now this workflow will always exist you don't have to do if workflow doesn't exist because we are continuing to use find unique or throw and instead let's find all the nodes. Now let's go ahead and import the node type from generated Prisma like this. Okay, and let's do, my apologies, we also have to include the nodes. Include nodes true connections true. And I think that maybe I'm telling you something incorrect here. Yes, it makes no sense that this is a node. So what are we actually doing here? Why am I getting confused? What I thought that we are doing now is just, you know, fetching the nodes from the server. But here's the thing, the structure that React flow expects, and the structure that we save in our Prisma schema are not one-to-one mappings, which means that now what we are doing here is transforming the server nodes to React Flow compatible nodes. That's what we are doing, which means that this node type should more specifically come from react flow so type node from xy flow react and this will be a type of edge the other one great so make sure you have added both node and edge head back instead of get one and now in here what we are going to do is we are going to transform the nodes. So workflow.nodes, which now exists because we include them, .map, get the individual node here, give each node an ID, which we have, give each node a type, which we also have, and give each node a position. Let's go ahead and also wrap it up by giving each node its data. Now in here, we have some type issues, I believe. The position is incompatible. That's because the position is a type of JSON value, which is technically correct. But let's be a little more specific. It's going to be a type of number and a type of number, the coordinates. And this one is the second one that's problematic. So this one is, yes, a little bit problematic. So let's go ahead and do node data as record of string or unknown because literally anything can be data or just an empty object. Perfect. Now let's go ahead and let's transform the server connections to React flow compatible edges. So const edges is a type of edge, which is an array. Workflow.connections.map. Get the individual connection. And let's go ahead and add the following things. ID, connection ID. Source, connection from node ID. Target, connection to node ID. Source handle from output. target handle to input. This way we can always transform from the server to the client. So now we have nodes and edges which are ready to be passed down and rendered in React flow. So let's go ahead and modify this by doing return id workflow.id name workflow.name nodes edges. There we go. Now that we have that inside of editor.tsx, we can modify the default values here. So instead of using initial nodes and initial edges, let's do workflow.nodes. And let's do workflow.edges like this. And we can now remove the unused variables from here. What I want to do now is also start npxprisma.studio. Let's go ahead and open up the studio here. And let's remove all of the existing workflows. This way we won't have any outdated workflows which don't have initial nodes. So just delete all of them. Once you have deleted all of them, let's go inside of workflows and make sure to refresh this because it might cause some errors. So there we go. No items. And when I click add item, what should happen now is that I should have an initial node and you can see that they do have it, but it's weird. It doesn't exactly look as expected. Nothing is really written here, but something was loaded here. So what we have to do now is we have to create something called Node Components Registry. So let's go ahead and do the following. I'm going to go ahead and extend this. And I'm going to go inside of source config. and in here I'm going to create nodeComponents.ds and I will export const nodeComponents as a factory map. And in here I'm going to do when we use node type from Prisma initial, let's render initial node. and let's go ahead and do as const satisfies node types from React flow. So make sure to import this as type and I think, okay, no, this is not import as type. So obviously, we're now going to have to create the initial node and let me just go ahead yeah let's also just do a quick type export which we're going to need later registered node type to be key of type of node components so in order to create the initial node we're going to need a little help specifically help from react flow and chat cnui that's right so you you go on to React Flow and click inside of UI, in here, you can go ahead and you can find, where is it? Custom nodes. And you can find the placeholder. And this is how it looks like. And that's exactly what I want. I want this to be our initial node, just a big plus button. In order to do that, we have to run npx chatcnlatest and then add the placeholder node. So let's go ahead and do that. So I will specifically continue to use my version of ShatCN, which is 3.3.1. I would suggest that you also kind of try and use the same ShatCN version, even though I think this will continue to work even if you use an updated version now. So even using at latest should work. And you can see how we specifically use this placeholder node. Now, if this fails for you, I will try and put a link on the screen where you can find exact code of this component, right? Because that's all this is. It will just add one file to our project. So if I press enter here, let's go ahead and see. This should now add placeholder node. There we go. It added two things. It added base node and placeholder node. Now, I want to make sure that we know this was added with a CLI, that we didn't write this. So inside of components, I will create something called react-flow like this. And I will select both base node and placeholder node and I will paste it here. Simply so I don't confuse them and select yes for updating the imports. So let's go inside of base node and let's just see, okay, imports are fine. Placeholder node, you can see it's having problems. So let's just change this to its neighbor like this because we just moved both of them here. I feel more comfortable this way. So I know exactly these are the ones that I made myself. These are the ones from ShatCN and these are the ones from React Flow using ShatCN registry. Now that we have base node and placeholder node, inside of components, I'm going to create the initial node. What is the initial node? The thing we just specified here, which doesn't exist yet. So inside of components, let's create initial node.tsx. Let's mark it as use client. Let's import type node props from xyflow react. Let's import plus icon from lucid react. Let go ahead and import memo use state from react Let go ahead and import placeholder node from slash react flow placeholder node like this Let export const initial node to be memo props to be a type of node props like this. Let's go ahead and define initial node.display name initial node and let's go ahead and return inside of here placeholder node and inside let's add a div and a plus icon give this a class name of size 4 give this a class name of cursor pointer flex items center and justify center go ahead and spread all the past props to the placeholder node description will be add a first step here uh let me just see do we need description um no let's let's remove it no need for a description here so just a big plus icon and i think that actually uh might be enough now that we have the initial node we can import it from components initial node and now that we have the registry node components which is mapping prisma type initial to a component initial node and we use the placeholder node from the chat cn installation you can go ahead inside of source you can go inside of features editor editor.tsx and let's go ahead and add node let me just go ahead and find what the name of the prop is so it's called node types and passing node components config here so just the thing we've created where we map the prisma type to the initial node here and now if we've done this correctly there we go you can see how the initial node looks now. So now every single time you create a new workflow, you will have this new placeholder node, which we are going to use that when clicked on, simply opens the sidebar node selector, which will give the user much more options, right? But that's what I want for the node and basically what we've learned here is how easy it is to add custom nodes to react flow which is what we are going to be heavily using in this tutorial so we are going to create a registry of node components and then inside of prisma you know later in node types when we add open ai we are very simply going to copy this, change this to open AI and this will be open AI node. That's how it's going to work and then React Flow will know exactly what to render if the type is set to open AI. I purposely did this with initial so it's the simplest possible example because this one literally displays nothing, has no data. It's just a different looking node than the other ones. And I just want to show you like one more thing because I'm not sure if all of you completely understood what we did here. We are no longer using the initial nodes constant or the initial edges constant. We are literally fetching from the database about our current node state, right? So if I go inside of use suspense workflow, inside of get one, and then find my create here, right, where I create these. Let me just check. How do I create many? Okay, create many and then like an array. Is that how you use Prisma? Okay, let me just quickly find how I do this. okay I think that create many is also an object and then uses data and then you open an array yes so like this so instead of just adding like one initial node if you were to copy this paste it and created another initial node but gave it a position of 200 and then went back to workflows and if you click new workflow that should give you two initial nodes every time you create so that's what i'm trying to explain right we have successfully connected our database state with our canvas state so we will now work from that principle and it will be very easy for us to translate whatever we make within this canvas into nodes that we understand on the server that we can easily easily turn into background jobs that we can easily update remove delete right it's all going to be in sync so let me revert this quickly back to just a normal create. So just create with a single initial node. Perfect. Now before we wrap up let's just enhance the look of our nodes even more because what we're going to do now we're going to reuse in every other node. Inside of components let's go ahead and create workflow-node.tsx. And let's go ahead and mark it as use client. Let's go ahead and let's import node toolbar from xyflow react and position from xyflow react. Let's import settings icon from lucid react and trash icon from lucid react. Then let's import type react node from react and let's import button from UI button. In here, let's go ahead and create an interface workflow node props. Let me just fix the way I did this. So interface workflow node props, which will accept children, which are a type of React node. And then let's go ahead and do show toolbar to be an optional boolean on delete to be an optional function then the same for on settings name optional string and description optional string as well and then expert function workflow node. Let's assign workflow node props here. Let's go ahead and destructure all of the props and set the default show toolbar to be true. And then in here, let's go ahead and let's return an empty fragment. And let's check if show toolbar is set to true, in that case, render node toolbar. And inside of here, add a button which renders the settings icon. Give it a size of small variant of ghost on click on settings. And the settings icon shall have a class name of size 4. Duplicate this exact button and change this to be on delete and trash icon. After the toolbar, render the children and then if a name is present, render node toolbar again. Give it a position prop of position.bottom is visible, class name maximum width of 200 pixels and text center. Then a paragraph here which will render the name which we pass and a class name font medium. Then let's check if we have a description and if we do a paragraph which will render the description. This will have a class name of text muted foreground, truncate and text small. This will be very useful when we for example have a node named open AI and then a description for example not configured or if it is configured we will show a brief description of what we entered as the system prompt, for example. And the toolbar above will be used to quickly access the settings of that node. So now that we have this, let's go ahead and go back inside of our initial node here. And let's wrap it inside of workflow node from dot slash workflow node which we've just created so wrap the entire thing in it like so and now when it's rendered here whoops I have to find a way to turn off that agent thingy let me refresh to see if we can maybe already see something uh all right so now on click this just becomes a node uh yes not not exactly what i wanted to demonstrate yes i think placeholder node is like the worst node to demonstrate this into but let's just for now end the chapter by adding show toolbar and set it to false just so i don't forget to do it later. I know this makes no sense. I made a mistake. I should have maybe introduced this in another chapter. But the good thing is we are going to use this as soon as the next node is created. And it wasn't really too complicated. I'm just disappointed that you can't see what we did.\nbecause this way it's hard to develop. You don't even know what you did, right? So for now in the on placeholder node, let's see if I add on click, can I maybe just like override it? So it cannot accept on click. And let me see. Let's go inside of this React Flow placeholder node. I think we might need to create some adjustments here. so what I'm going to do here is I'm going to add on click to be this and now on click should be passed further down let me see okay so this is the base node we have handle we have source okay we have handle click here ah yes but you can see that handle click what it does is it creates this new node. So we're actually going to remove that functionality. Yes, let's go inside of placeholder node. And we can remove everything here, really. Everything. We don't need a single thing. The only thing we should do is add on click here and paste it here directly. Let me see. The handle is fine. This handle is fine too. This is all right. Let's go ahead and change this from width 150 pixels to be width auto and height auto as well. Border dash, border gray 400, BG card, padding 2. Let's change this to padding 4. Text center, text gray 400, shadow none. Let's do cursor pointer. Let's go ahead and give it that. Hover border gray 500. Hover bg gray 50. Let's go ahead and add this. Let's remove the unused use react flow and unused use node ID. And let's remove use callback. and now instead of the initial node you will see that the placeholder node now accepts its own on click so if I go inside of my workflows here now and just create a new workflow I should now get the one with the normal initial node in here and there we go looks much better and clicking on it does nothing which is exactly what we want what it's going to do later is it's going to open a sidebar. So we have overridden the initial functionality of the placeholder node that was added in here with the command line interface so that we can take care of the on click from the outside. And I think that was kind of the problem why we were not able to demonstrate workflow node. Let me try and remove this. And there we go. You can see that now when you click on it, So if you click outside, it doesn't exist. When you click on it, you can see the settings and you can see the delete option, right? And if I were to give this a name, initial node, you can see it's displayed here. And if I do description, click to add a node, you will see it has a description. There we go. So that is what I wanted to demonstrate. Now I personally think the big plus button is descriptive enough. So for that reason, I chose to do show toolbar set to false here. And because I mean, this seems pretty self-explanatory, right? When we click here, it's just going to open a big sidebar node selector. Perfect. So that is what I wanted to show you. And I'm so happy that I was able to do that. Let me remove useState from React here. It seems to be an unused import. We're going to leave this as it is. And I'm going to wrap the chapter up by adding just one more thing here. Inside of features, editor, editor.tsx, let's add one more thing here. So we're going to learn how to add a custom element inside of the canvas because all of these three elements were built in. So what if you just want to add like a random plus button here at the top? Why would we need that? Well, once we add something here, this initial node will no longer exist. So we won't have any way of adding new nodes. Because of that, we need one more way to add new nodes. So let's go ahead and go inside of editor component. and create addNodeButton.tsx. Mark this as useClient, import plus icon from Lucid React, import memo and useState from React, and import button from components UI button. export const add node button make it a memo component and return button with a plus icon here and go ahead and give this button an on click for now to just be an empty arrow function size of icon variant of outline class name bg background like this and let's do add node button dot display name add node button now that we have this you can remove use state we will have it later when we actually enable this now let's go ahead and actually add this we can do that by using panel from React Flow React. I mean XY Flow React. And go ahead and render add node button in here from dot slash add node button. And to define the position, all you have to do is define it. And you can see that once I do that, I have a new button here in the corner. So now even if the initial node goes away, I still have the plus icon here, which is always fixed on the screen, as you can see. Perfect. So I believe that that is a good place to end this chapter. We got familiar with React Flow. We learned how to connect nodes using code, right? Using those initial constants. But we also learned how to load the React Canvas state from our server and how to render completely custom node components which didn't exist initially. We also created some reusable toolbars which will be very helpful later. And we also started separating what was added from ChatCN and what we built ourselves from scratch. Amazing, amazing job. I think we've done more than enough now. So in the next chapter, we are actually going to learn on how to create the node selector sidebar, which will allow us to select some trigger nodes like manual trigger and then some execution nodes like OpenAI, Gemini, Slack, Discord. So let's go ahead now and see what was the plan here. We updated the schema, node table connection table and we load the default editor state let's go ahead and push this to github so 15 editor i have 12 changes here some of them were migration schemas here all right so i'm gonna head and create a new branch 15 editor i'm going to click stage all changes and then 15 editor. Let's hit commit and let's hit publish branch. Now as always I'm going to go ahead and just create a new pull request here and since this was a lot of new things it would be a good idea to have another pair of eyes take a look at it. And here we have the summary. New features. We introduced an interactive workflow editor with a node-based canvas, including pan and zoom controls, background grid, as well as a minimap. New workflows now start with an initial node displayed on the canvas. Nodes feature a toolbar with settings and delete actions. Optional name and description are shown in a bottom label. We added an add node button in the editor panel, UI only at this time, meaning no real functionality here, as well as some visual refinements for node structure and placeholders to support future node types. Exactly. As always, file by file walkthrough here and some interesting sequence diagrams. So the first one is explaining how when we fetch one workflow, we attempt to find it or we throw an error. And then if we do find it, we make sure to include workflows, nodes, and connections. And then we map that to React flow type of node and edges. And when we return that, we can properly render the React flow canvas and get exactly what the user expects to get. And in here we have a simple diagram explaining how when we create a new workflow we also create the initial node with it. And for the comments not too much actually. I made a typo in my workflow node. I misspelled truncate. And down here it is telling me that I should find a way to persist node edge state to the server. That is correct. Remember, we do have a save button in the workflow header, but we never actually enabled it. We will work on that later. But yes, this is a good comment, but we are on our way to fix that. So let's go ahead and merge this pull request And once it been merged let go ahead and go back instead of our main branch Make sure to synchronize your changes and once your changes have been synchronized as always, confirm with the graph. 15, editor. Perfect. Merged into main. That marks the end of this chapter. Amazing, amazing job, and see you in the next one. In this chapter, we're going to continue working on the editor component by introducing the node selector, which will give us an ability to add all types of nodes to the canvas. We're also going to add our first trigger node called manual trigger, which will allow users to manually start executing a workflow. And we're going to add a very simple execution node called HTTP request, which will be able to execute HTTP requests. We are, of course, going to create the actual node selector component. And more importantly, we're going to establish the editor state. And once we have that editor state, we're going to enable editor save functionality. Let's get started by adding two new nodes to our project. So inside of Prisma schema, inside of node type, where last time we just added initial, let's go ahead and let's extend it by adding a type manual trigger. And then let's also add HTTP request. So now instead of just having one node type, we have three node types. Let's go ahead and let's do npxprisma migrate dev. And we can just call it more nodes or new nodes. so here we have the prompt for a name new nodes and now we are ready to create the node selector component. I recommend restarting your Next.js server while you do this every time you update your Prisma schema do restart your server it's definitely a good thing to do otherwise you might get some weird errors which you think maybe you will think you did something incorrectly but when that's not the case. It's just that the cache is weird or something. Let's go ahead inside of components and let's create a new file called node selector.tsx. Let's mark it as use client. Let's go ahead and let's import. We don't really have this package, but let's go ahead and finish the import and then we're going to add it. So we are going to import create ID from this package, which allows us to create CUIDs. Why CUIDs? Well, because we use CUIDs everywhere. And this is by far the most popular package to do that, regardless if the name is not exactly, it doesn't instill confidence, right? But it is by far the most used package for this. So let npm install. My apologies, it is cud2. So make sure to install cud2 and make sure to add cud2 here. There we go. Then let's import use React flow from xyflow react. Let's go ahead and prepare from lucid react. Let's import a few of these. Globe icon, mouse pointer icon and we can remove webhook icon for now. let's import use callback from react let's import toast from sonar and now let's import everything we need from components ui sheet which is something that we've installed when we started this project this is kind of like a sidebar but not really like a drawer let's go ahead and let's import node type from Prisma and let's import separator from dot slash UI separator or you can do the components UI whatever you prefer. Let's go ahead and export type node type option give it a type of node type which we imported above, a label of string, description of a string, icon which is a React component type and each can have a optional class name prop or it can just be a normal string. So we are going to switch between components and images depending on what we need. Now let's go ahead and let's define a constant trigger nodes. Let's go ahead and give it a type of node type option and it's an array of those. And let's go ahead and add a type node type dot manual trigger. Label will be trigger manually. And the description can be whatever you want for example I'm going to use runs the flow on clicking a button good for getting started quickly icon mouse pointer icon great so that's all we need for the trigger nodes of course we're going to add more later now let's go ahead and do const execution nodes node type option like this the only reason we are separating these arrays is so that you can visually separate trigger nodes from execution nodes to not confuse people. Let's go ahead and give this a type of node type, HTTP request, label, HTTP request. Description can be makes an HTTP request, a simple one, and an icon of globe icon. You should already have it imported. Great. So now we have execution nodes and we have the trigger nodes. Now let's create the interface for the node selector component. So node selector props accepts open, on open change and the children. Then let's export function node selector, node selector props. Let's go ahead and let's get open, on open change, children. and then let's go ahead and let's go let's see what should we do first I think it's time to actually build this so we can see what we are doing rather than just doing business logic in advance so render sheets and pass open and pass on open change to be on open change then sheet trigger here is very simply going to be whatever we've wrapped the children around so this can be anything and make sure to use as child for that specific reason you're going to see what this will allow us to do in a second let's just render some sheet content here give it a side of right class name full width on small devices maximum width of md overflow y auto. Give it a header. Give it a title. What triggers this workflow? And then let's go ahead and give this a description. A trigger is a step that starts your workflow. And then outside of sheet header open a div trigger nodes dot map node type icon is going to be node type dot icon and return a div like this go ahead and give it a key of node type dot type let's go ahead and add a class name to be full width justify start height auto py5 px4 rounded none cursor pointer border l2 border transparent hover border l primary on click for now just leave it empty like this inside of here let's go ahead and add a class name flex items center gap six full with overflow hidden. And now let's finally render the icon. So if type of icon is just a string, it means it's an image href. So let's use a normal image element here. source is going to be the icon alt will be node type dot label class name is going to be size five object contain rounded small otherwise we are going to render it as an icon as a node with a class name size5. So I'm going to stop here so we can actually see what this even was. For example, let's go inside of our add node button. This is the last component I believe we have added in the editor. So you can find it in the editor here where we render React flow. we practiced adding like a random thing in the upper right corner so inside of source features editor components add node button we have it right here so what we could do here is wrap it inside of node selector like this and this will allow the button to become the trigger to open that drawer. Let's go ahead and pass in open on open change and let's very simply introduce a state here like this. Selector open, set selector open from use state react, pass in selector open and pass in set selector open in the second prop. Now that we have that let's go ahead and try it out So I going to refresh this And I will attempt to just you know click on the plus button here and there we go What triggers this workflow A trigger is a step that starts your workflow There we go. You can see how that looks. When I click on it, nothing really happens. So now let's keep that open. You can see it becomes full screen on mobile, and let's continue working inside of the node selector here. So after we render the icon, let's open a new div. Let's open a span inside, node type dot label. Now you can see we have a label here. Let's give the span a class name, font medium, text small. Let's give the parent div a class name, flex, flex column, items start, text. text left. Let's open a new span, node type description. Give it a class name of text extra small and text muted foreground. There we go. Now we can see how this looks with all of the details added. Perfect. And now what we can do is we can copy this entire thing. So yeah, let's copy the entire div like this copy it and paste it and then you're just going to change this from trigger nodes to execution nodes like this and between them you can add a separator like this if you want to you can also you know kind of like repeat the sheet header maybe you can do that instead of like separator. I don't know. Let's see. Maybe what executes this workflow. I don't know. I think separator is clear enough. Or maybe you can just change this depending on what type of nodes you have on your editor. You will be able to actually do that. You will be able to read the state of the editor so you can maybe see if you only have this default plus node, perhaps show a message what triggers this workflow and only show triggers and then once you start adding things also show both triggers and executions however you want i think this is okay now let's also make this button do that as well so the way we can do that is by going inside of our initial node component inside of source components initial node and just as we've wrapped our placeholder node within workflow node let's now do it with the node selector as simple as this they are in the same folder so they can be imported simple dot slash node selector and let's just go ahead and keep the state here selector open set selector open from use state from react just make sure you add that and once you have those two let's go ahead and just simply add open and on open change and now this big button here should serve as the on click as well I think we have to refresh here and click plus looks like that is not happening and it's very clear why because we never call set selector open to be true so now let's go ahead and click on this there we go And now that I'm actually, now that I think of it, maybe we don't even need to keep track of state from the outside because trigger will do that for us. But then it will be a bit harder to decide how to close it, I think. Let's just, let's keep it like this for now. all right perfect so now we have two ways one the initial way and one which will always be available here in the corner perfect now what we have to do is that when we click on trigger manually it should show the manual trigger node when i click on http request it should add that so let's go ahead and go inside of our node selector here and let's create the logic to do that so the first thing we have to do is we have to get the current state of the editor so let's call our use react flow which we have imported here and then we can get set nodes from here we can get get nodes and screen to flow position and now let's create handle node select let's make it a callback like this. And inside of here, let's also make sure this accepts a type. So node type is a type of node type option. Basically, the node type option is this, what we've defined above. So once we select one of those, first thing we're going to do is we're going to check if node type dot type is equal to node type dot manual trigger. Perhaps we can rename the node type in this case to be like selection, I don't know, selection dot type. Maybe that makes more sense. If selection dot type is same as the manual trigger, we have to check if there is an already existing manual trigger. right so you should never be able to add two manual triggers so what I'm going to do is do const nodes get nodes const has manual trigger nodes sum node node type node type dot manual trigger so is there any node already in the canvas which has a type of manual trigger. If has manual trigger, we are going to break this function by throwing an error. Only one manual trigger is allowed per workflow. And let's break the function with a return. Otherwise, let's call set nodes. Instead of here, we are going to get the snapshot of the current nodes. And let's go ahead into the following. Const has initial trigger nodes sum node node type node type dot initial so we are checking basically is this a state where we are adding the very first node so it's checking if this element exists because if it exists it means that whatever user just selected is the very first node that we are ever going to add so we're just going to replace the position of that initial trigger. Const center x is going to be window inner width divided by two and center y is going to be window inner height divided by two. And now let's go ahead and convert screen coordinates to react flow coordinates by doing const flow position, screen to flow position, x is going to be center x plus math dot random minus 0.5 times 200. And y is going to be center y. So what we're doing here is basically, if this is the logic we are preparing, later when you add like more nodes right um the way we're going to decide where to position a new node that's been added cannot just be a center because look at the canvas if i go you know all the way here and if i click add node i would expect it to appear here so we can't always just choose the center this is the center imagine always having to scroll back so that's what we are doing here We are using the current camera angle. Imagine it like that. We are using the current cursor position, the current canvas position, and positioning it a little bit off the center so they don't stack on top of one another. And let's go ahead and do const new node here. Pass in the ID to be create ID. So we can use create ID from cud to import. data will be an empty object position is going to be flow position type will be node selection dot type and now let's check if has initial trigger in that case let's just return new node because we don't have to like append this to a list of nodes we want to replace the existing initial trigger with this new node. And otherwise let's just return nodes new node in a sense that we're just adding more nodes here. And once we do that let's call on open change and set it to false in a sense that let's close the sidebar now. Now we have to add all of the dependencies here set nodes get nodes on open change screen to flow position great and now that we have that we can use that as the on click in here on click handle node select and pass in node type copy this and make sure to also add it in the execution nodes below there we go let's go ahead and refresh now and Now we should be able to add our node and this one should disappear. If I click this, there we go. The old node disappeared and this one was added. If I go and move here and do it again, you will see I now have two of them and they follow my camera, right? So if I initialize another one here, it's always going to be somewhere here. We slightly offset it randomly so they don't exactly stack on top of each other. okay this is stacking but at least it's not like this right all right looks like this is working obviously the problem is we didn't initialize any component that should load when the type is manual trigger or when the type is an http request so now let go ahead and do that let start by going inside of config node components here And let go ahead and let prepare node type dot HTTP request and node type dot manual trigger So the manual trigger one will be called manual trigger node. And the other one will be called HTTP request node. Let's start by creating the HTTP request node here. We're going to keep it simple. Though we will have to... All right, let's do the following. Leave this as error for now. Let's go inside of source, components, and create a new base execution node.tsx. Base execution node.tsx like this. Let's mark it as use client here. Let's go ahead and let's import. Type node props and position from XY flow react. Let's go ahead and import type lucid icon from lucid react. Import image from next image. Import memo type react node and use callback from react. Let's go ahead and now see do I have something called base handle. I only have something called base node, so I should be able to import base node and base node content from components react flow base node. It looks like both of them exist. Great. But we also need base handle. And I'm pretty sure that we can do that by going inside of here. Let's get inside of UI and let's try to find the handle here. Here we have it, base handle. so I'm going to go ahead and run this again again if you if for whatever reason this is not working for you using the link on the screen or the place basically my assets folder you can find all the components which we add in this way so you can follow along npx chat cnui I'm using my version and let's add base handle. There we go. It's installing dependencies. And once we have the base handle, what I'm going to do immediately is move the base handle inside of React flow, move. Now, I'm not sure if only base handle was added, but I think that was the only component that was added. Great. Now we can focus back here in the base execution node. And we can now also import the base handle. So import base handle from React Flow base handle. And you can also change this to be like this, like a shorter line. All right. And let's import workflow node from .slash workflow node. now let's go ahead and let's create an interface here base execution node props extends node props so node props was imported from here the only one we won't implement for now is the status because it will require it will require another set of components and i think the best way to implement the status thing is when we implement the real-time pub sub messaging because then you will actually be able to see like loading status, error status or success status. So I don't really want to waste time developing that now because we won't really be able to see it in action. Let's do export const base execution node. It's a memo like this. type base execution node props and open a function like this. All right. Now let's go ahead and extract ID icon. Let's remap it to icon with a capital I, name, description, children, on settings, on double click, and I think that's it. Great. now let's go ahead and let's return workflow node let's pass in the name to be name description to be description on delete to be handle delete on settings to be on settings let's go ahead and quickly just define const handle delete here as an empty function then let's render the base node let's pass in the it should have on double click it does great on double click so on double click is just a native html event handler that's why it's working it's just a normal div but it works just as the same way as onclick does, right? And inside of here, let's do base node content. And then let's go ahead and check if type of icon is a string. Let's render an image with a source of icon. alt of name with 16 and height 16. Otherwise, let's render it as a node. So depending, are we going to use an icon or are we going to use an image? Great. After that, render the children. And now we have to define the base handles here. So render base handle, it's a self-closing tag. let's go ahead and give it an id of target1, let's give it a type of target and a position of position.left. So we imported position from xyflow react, duplicated the base handle and give this one a source of one and the type of source and this will be position right. and let's just end it by giving it a display name like this. So what's currently missing is the status as I've described, but we are going to come back to this later. All right, so base execution node and obviously we are also missing this. So let me just add to-do, add, delete. and inside of here I'm going to do to do wrap within node status indicator which is something we're going to focus on later because we don't really have a way of displaying it right now. Let's remove the unused use callback. Now that we have the base execution node we can go ahead and develop the HTTP request node. So I'm going to go ahead inside of the following features. Let's create a new folder called executions. And let's go ahead and create a new folder called components. And let's go ahead and create a new folder called HTTP request. And in here, we're going to create all business logic for HTTP request as well as all UI things. So let's do node.tsx like this. Let's mark it as useClient. Let's go ahead and add some imports. We're going to need node and node props from XYFlow React as well as useReactFlow. Let's import globe icon from lucid react. Let's import memo and useState. Let's import our newly created base execution node. You can also simplify this by doing this, I believe. Maybe not. Yeah, let's go ahead and do this. Instead of components, remove the base execution node from here. And let's keep it inside of executions components. And if it asks to update imports, you can select yes. so inside of base execution node you can now replace this with the alias because it makes more sense now and now you should be able to find the base execution node as your neighbor because it will exclusively be used inside of executions here and in here we are going to we are going to use the same way we will build the http request we are also going to build open ar we're also going to build Anthropic, we're going to build Gemini this way. So that's why I want to keep this close instead of just like randomly, as in it can be used for whatever. So instead of HTTP request node.dsx, make sure that you have this now. And now let's just focus on creating HTTP request node data. Endpoint is going to be an empty string method is going to be either get or post or put or patch or delete. Body is going to be an empty string and then whatever other properties we might need. Now this data will require a separate modal to be worked with but let's already define it here simply so you can see that we will have some actual functionality behind these nodes. Let's define HTTP request node type to be a node with HTTP request node data. So that's how we are going to specify a more specific type of node with specific data we expect. Let's export const HTTP request node to be memo, props, node props, HTTP request node,\nOh, we can actually, no, it needs to be node props, HTTP request node type. All right. And then in here, what we are going to do is we're going to do const description to be node data question mark. Let's see how do we get node data specifically. Just a second. Okay. Let's first define node data to be props.data as HTTP request node data. in the description, let's do node data dot endpoint. Now, if we define an endpoint for this HTTP request node, we are very simply going to render the following node data dot method that we used or fallback to get. And then we're simply going to display node data dot endpoints like this. Otherwise, we're going to say not configured. So basically, if I know this is a lot right now, and the reason it's complicated is because I just told you to write this entire thing, but you don't really understand where you're going to add these fields, right? There will be a dialog that will open when you double-click on HTTP request node and then you will be able to have inputs and forms to select these things. And depending on that input, we are choosing what to display on this node, as in not configured, like you haven't done any settings here, or I will show you the exact method you chose as well as the endpoint you chose. So now let's finally do a return here so we can see what's going on. So open a fragment and render a base execution node in here. And that's the cool thing about developing this base execution node. We are going to reuse it for every single node that we have, which is executing something. So now we're just passing some things here. Icon will be globe icon. Name will be HTTP request. Description will be dynamic, so either not configured or the full option that we did. On settings for now empty. On double click for now empty. So the double click will open the dialog or settings. Both of those will basically open the dialog and then the user will be able to configure the endpoint, method, body and key. But we needed to like develop this because you can see we need it for node types. And let's just end this by adding the HTTP request node to be a display name like this. Great. so for now let's remove the unused use state and use react flow here now that we have this we can go back inside of node components here and we should be able to import http request node from features executions components request http request node now we have to do the same thing for manual trigger node but let's comment this out just so we can see what we developed here so i'm going to refresh and then i'm going to select an http request and this is how it looks like right so http request with a globe icon and it says not configured because it isn't you will have to double click on it or click on the settings button which will open the dialogue and that will basically allow you to pass in the method, which is get put post patch or delete, and the endpoint, which is the URL, as well as some other body configuration. And now we should do the same for this, because you can see how this one is not really configured at the moment. In order to develop the trigger node, we have to also create the base for it. So just as we've develop the base for the execution node, let's copy it and let's go ahead inside of features here and let's actually create a new folder called triggers and in here let's add components and then inside of here paste the copy of base execution node and rename this to base trigger node and now inside of here make sure that you're inside that new file so you don't override what existed, right? Let's go ahead and change this from base execution node props to base trigger node props. Icon is going to be the same name, is going to be the same description, will be the same children. We're going to comment out on the status. Everything else will be the same here. So instead of base execution node here, again, trigger. Same for the props here. We're going to leave this to be just empty and we're going to work on it later. Workflow node, same thing. It will have its own settings, it will have handle delete, it will have description. We will also need to wrap it within a status indicator. Now what we are going to start to do differently is this. The base node will need a class name of roundedL to Excel. Relative and the group. Let me fix the typo relative. There we go. Rounded L to Excel. All right. Now instead of base node content, this is fine. Let's see. So we need source, we need alt, we need width, we need height. All of that is good, I think. Icon is good as well. Children is good. And now here's the crucial difference. You won't be able to connect to the base trigger from the target handle. So remove it. It should only have a source on the right side. And let's go ahead and of course change these two to be base trigger node. There we go. So very simply we copied the base execution node. We renamed it in all instances. We gave a class name to base node here and we removed one base handle which was the target one. So a slight modification but quite easy to build. We didn't have to do it from scratch. Now inside of triggers here, inside of components, let's create a new folder called manual trigger like this. And this manual trigger will now have node dot tsx let's export const manual trigger node give it a memo with props props node props from xyflow react and this one will not have any settings like our http request did this is just a normal on click event nothing special here really so let's go ahead and just do a very simple return let's return a fragment because we will have more elements later base trigger node and let's spread the props icon will be mouse pointer icon from lucid react name will be when clicking on actually let's just do when clicking and then in quotes execute workflow like that. Let's go ahead and do this. Let's pass in status to be node status and comment it out because we will need it later. So I will just add to do so I don't remember. On settings should just be, it should be handle open settings. So I'm going to comment that out too as well. And on double click again handle open settings to do so i don't forget those things now that we have the manual trigger node let's go inside of node components here let's enable it and let's import from features triggers components manual trigger forward slash node and there we go you can immediately see how this looks so when clicking execute workflow that's how this is looking right perfect so now you know how to you know create a sheet which can render new elements here on the canvas and if i try to add one more trigger i should actually get an error only one manual trigger is allowed per workflow so i copied that from n8n uh if you personally think for your use case you should have multiple manual executions feel free to remove that if clause that i added in the node selector. Great. So obviously still room for improvement here. Still a lot of confusion about why did we need all of that inside of, you know, base execution node. How will we actually open the dialogues? How will we delete nodes? A lot of questions for sure. But let's go ahead and see where we are right now. So we added the manual node, HTTP request node, and we built the node selector component. All of this, I have to say, did take more time than I thought. So I think it's best that I leave the save functionality for the next chapter, simply because in that next chapter I will also be working on creating the delete functionality. So perhaps that will be kind of be a chapter where we manipulate the state of the editor. So we leave this one as a simple, okay, I learned how to create a node selector and how to remove the current initial node and how to render this executor style thingy. all right so yes this looks good we're also going to kind of modify the css here a little bit i don't i'm not sure i like them this dark uh i will see uh how we're going to fix that great so let's call it a day here and i'm going to prepare some things for the next chapter so let me go here 16 node selector I gonna go ahead and commit this 13 files that I have here I going to create a new branch 16 node selector And I'm going to go ahead and stage all of my changes here. 16 node selector, commit, and I'm going to publish the branch. Now let's go ahead and take a look at GitHub. Let's go ahead and open a pull request and let's review our code. And here we have the summary. We added a node selector panel to insert workflow nodes from the canvas to add button. We introduced new node types, manual trigger and HTTP request. We added base components for trigger and execution nodes with consistent styling and connection handles. We enhanced initial node to open the node selector for quick starting points. We mapped new node types to their visual components in the editor. We also added a lightweight ID generation library, and we updated the database schema to support new node types. Now in this specific pull request, I don't believe there is anything too useful to look at a sequence diagram for, but we do have some comments. So the first issue is CodeRabbit suggests not dropping all the nodes. Instead, replace the initial node instead. So instead of just doing return new node or this, we find the initial node and replace it instead of the has initial trigger block. This is what I actually did initially. But the problem is if you scroll away, like drag the canvas away from that node, Let me show you what I'm thinking about. So if I refresh this, what it's suggesting is that this exact position gets replaced with the initial node. Right now, that doesn't happen. You can see how it's a little below it. It's basically random every time. The reason I do that is because if you do like this and then add it, it's still going to be here. Otherwise, if you do what CodeRabbit suggests, it will have to be here. You'll have to go back here. So that's why I'm not doing that. All right. In here, it noticed a bug. We have double spread of props here. So definitely a bug. But as far as I know, since this is in React Flow folder, it means it was added by them, not us. So at least we didn't write the bug. But we should definitely be the ones who fix it. In the add node button here, we left an on click completely empty. and we didn't notice because it's still working. The reason it's working is because we wrapped it in sort of a node selector and we gave it an on open change here. And node selector is wrapping the button with a sheet trigger and I think that that trigger for itself calls on open change regardless that this is empty. I will make sure to take a look at this before the next chapter to give you a conclusion on whether we should remove this or maybe switch this selector open to be true here. I'm not sure. But you know, this works. So I'm not sure what exactly to conclude. But yeah, we could also add this, it wouldn't hurt, or we can just remove the onclick altogether. And now most of these things that it's telling me are because of the unused on double click, empty, handle deletes. I'm not really going to take into consideration those comments now because this is still in to-do. We still have to properly use them. So these comments aren't really applicable right now. Now in here, it told me to fix the node props generic and avoid unsafe cast. And I think that might be true Because I'm doing HTTP request node type by extending node here, and then I'm using node props to extend it using this. I will see if that's really all I need. So in here, it suggests doing node props and passing. Wait, but what am I doing incorrectly? Oh, I misunderstood. I thought it was referring to how I initialize the data, but it's referring to this specifically, to avoid casting like as HTTP request node data and making it work by default. All right. So yeah, definitely if we can do that, that would be an improvement. I will look into how we can fix that. And the rest of these are just the same about our unused handlers. So let's just merge this pull request here for now. And I will make sure to take a look at this fixes in the next chapter. Let's go back inside of our main branch here. and let's synchronize the changes. Again, amazing comments by CodeRabbit here. And let's confirm we've merged by going inside of Graph. There we go, 16 node selector. So we didn't end up developing the editor state, and we didn't enable editor save functionality. We will do that in the next chapter, but we did finish the rest. Amazing, amazing job, and see you in the next chapter where we're going to explore the editor state, the save button, how to delete nodes, etc. Amazing job and see you in the next chapter. In this chapter, we're going to be working on the editor state, which will include adding the save functionality, delete functionality for each node, and the settings functionality for each node. We're also going to fix some of the bugs which CodeRabbit all request review noted last chapter. So let's start with that. We have an issue of empty on-click handlers and a specific typecast which we might not need. So make sure you have your app running and let's take a look at one of the examples here. So right now if I click on this button, node selector opens. And if I click on the one in the upper right corner, same thing happens. Let's go ahead and actually look at the definition. So if I go inside of my initial node in source components here, you will see that I use the set selector open both here and the node selector, but I also do it here in the on click. now the reason we need it in the on click here is because we specifically want the placeholder node click to trigger the node selector and not the workflow node which is essentially as you can see just some node toolbar which renders the children so because of that reason we specifically need an onclick of this component to open it. But an interesting case happens in our add node button component. In here, we actually have an empty onclick. So why is the node selector opening? Why is this working? That's what CodeRabbit proposed as a fix. Like, hey, you are missing set selector open set to true here. If you change this, nothing will change. It just works. but it also worked before. Why? It's because of the way Node Selector works. If you go inside of Node Selector, you will see that we render the children inside of Sheet Trigger. Now, this Sheet Trigger basically acts as a button which has Set Selector toggle, right? So I believe that even if we completely remove the on click, we can just rely on the fact that we passed on open change here. So if I try now you can see it works just as well. So if for whatever reason yours doesn't work or if you just prefer being explicit you can go ahead and do set selector open here and set it to true. If that's what you want right. But basically this sheet can communicate with the trigger and once the trigger has been clicked, it will call the onOpenChange that we pass here and it will execute it. Again, the reason that doesn't work here is because it registers this as the trigger because that's its nearest child. But we want it to be active on the placeholder node. So because of that, it's not working as expected. If you disable the onClick on the placeholder node in the initial node component, you can see that nothing really happens. because it's expecting a click on the workflow node, not the placeholder node. Now you could potentially move node selector here. And I think that then maybe it will work. I'm not sure. I haven't tried. Let's see. Yes, then it does work. But I'm not sure what are the implications of node selector being wrapped inside of the workflow node. I'm not sure. but yes I built the entire project like this so I'm going to stay true to my source code but yes you could just move node selector to wrap the placeholder node and then users click on the placeholder node will trigger this the same way the individual on click does. All right once we resolve those issues which okay we resolved one of them so empty on click handlers. Now let's do typecasts. So I think that's just a unneeded cast we added. If we go inside of HTTP request forward slash node, so inside of features, executions, components, HTTP request folder, for some reason, we define node data constant and we give it a cast. I think we can just remove the cast and when I hover over this it is HTTP request node data which is exactly what we casted it as. So this is obviously working as expected. No errors are being shown which means this works just fine. So we can go ahead and mark this as fixed as well. Now let's go ahead and let's implement the save functionality. So I want to start that by going inside of the workflows feature router, basically the trpc router. And just as we have create and just as we have update name let copy update name because it the most similar one and let change it to be update It going to be a protected procedure and the input will be a little bit different So I'm just going to collapse this because the object will be quite larger than before. So we are still going to need the ID, but we are not going to be working with name. Instead, we will receive a bunch of nodes. In fact, it's going to be an array of nodes. inside of that array we're going to have some objects each object will have an id a type which is a z.string or it can be nullish then we're going to have a position for each node which will be an object again with x and Y. After position, we're also going to have some data, which will be a record. And then we're going to set the key to be a type of string. And the value can be a type of any because the data can literally be any. And it's also optional. And besides receiving nodes, we're also going to be receiving edges. So we are receiving this from React Flow. So don't confuse these values with our database of nodes and connections. We are now going to have to transform this value into database-compatible values. So the objects that will be stored inside of edges input will be source, which is a type of string, target, which is a type of string, and source handle, which is string nullish, and target handle, which is the same. All right, that is our input here. So now let's go ahead and start by extracting the fields here. ID nodes and edges from the input. Make sure to mark this as an asynchronous mutation. You can remove this for now. let's go ahead and see if we find the workflow or not by using id and user id we can change this to find unique or throw so this will always be an existing workflow because if you just use find unique then it can be null and then you have to manually fix that so just make sure you throw otherwise because we will handle that error and now let's go ahead and let's make sure to open a transactions so we ensure consistency. So let's return await prisma.$transaction asynchronous open a transaction and let's go ahead and start doing some things. First things first we're going to delete all existing nodes and by deleting nodes for this workflow that's also going to delete connections because we are using cascade instead of prisma schema. If you take a look at nodes. My apologies, I think we need to take a look at the connection. Yes. So if from node or to node is deleted for that connection, it will automatically remove itself from the database. So what we have to do to clean this up is just go ahead and use the transaction to delete all nodes associated with this workflow ID. There we go, just like this. And then we have to create new nodes. so now we're going to use this input here to create nodes compatible with our database so create many data nodes.map get the individual node and return an immediate object now it's passing the id workflow id will simply be id name will be node type or unknown type will be node dot type and now we have to fix this by manually casting as node type looks like we already have node type imported from prisma perfect add position to be node dot position data to be node data or an empty array and now let me go ahead and fix the this to be dot node and I believe that's all we need to use the transaction to transform whatever input we received from react flow editor into our database compatible nodes and now we also need to create connections so let's go ahead and wait tx.connection.createMany data edges.map get the individual edge and return an immediate object. Now inside of here workflow ID will be ID from node ID will be edge.source to node ID will be edge.target. From output will be edge source handle or main. To input will be edge target handle or main. Great. And since this transaction doesn't really modify the workflow directly, just its relations, it will never really appear as modified. So we have to end this by also updating the workflow's update at timestamp. Otherwise, it will be bad user experience, right? You just created some nodes in a workflow, you clicked save, and in the list, it doesn't appear as updated. So let's just do await txworkflow.update where we found the matching ID data updated at new date. Just like this. And finally, return workflow. And just like that, we now have an equivalent update procedure which will transform React flow values into our database compatible values the same way, which is the opposite, right? Inside of get1, we transform server database nodes into React flow compatible nodes. Inside of our update, we do the opposite. We transform React flow nodes into database compatible nodes. Perfect. So now that we have that, let's go ahead and create use update workflow. So instead of use workflows, where we develop use suspense workflow, use update workflow name, let's go ahead and in fact copy that specific one. Let's paste it here. So this will be a hook to update a workflow. So this is located in here. Let me show you features, workflows, hooks, use workflow. Use update workflow will have query client, ERPC, and it will call workflows.update. And this will say updated, or let's actually call saved, because that's kind of the action that will happen. It will save, right? So what do we want to invalidate? We do want to invalidate getMany, and we want to invalidate this one. Yes, perfect. And this will be failed to save workflow. Excellent. So not much changing needed in this case. Perfect. Now that we have that, we have to go ahead and implement some kind of state management. Why do we need that? Well, take a look at what we currently have. So right now, the save button is in a component called workflow ID header. How did I call it? header. My apologies, I called it editor header, as you can see. Now the editor header doesn't really have access to my editor component state. So in order to pass that around, I can either create some kind of context, I can do some prop drilling, or I can just install one of the state management libraries. Now the one that I found myself using more and more is yotai. So I'm going to go ahead and do npm install yotai. The reason I like using it is because the syntax is almost the same as just using normal use state, except it's global and it works everywhere. So I kind of really like that. Let me go inside of package.json, search for yotai so you can see the exact version I'm using just in case you're wondering. And now let's go ahead and let's create a store for this. so I'm going to go ahead right here inside of source features editor I'm going to create store and I will create atoms.ds and then in here I'm going to import type react flow instance from xy flow react I'm going to import atom from yotai and I will export const editor atom to be atom which is a type of React flow instance or null if it wasn't initialized. And then I will be able to share this atom. I will assign it inside of the editor component and I will fetch it inside of my editor header component. Now, there is just a quick note here. If we go to Yotai documentation here, you can use the link on the screen to visit it. And if I go inside of docs here, we usually have a provider here but i never i don't think i really understood when we are supposed to use it and that just crossed my mind here so let me go ahead and find next js guide okay so this works this works yes but because the problem is i can use the provider or i don't have to use it so let's see okay so to limit the lifetime of the store to the scope of one request you need to use a provider at the root of your app or a subtree if you're using Yotai only for a part of your application let's go ahead and just add this I think it will make our app behave better so instead of layout instead of the app source app I'm just going to import this let me move globalist all the way down here I think that should be down so provider Yotai and let's go ahead and just add provider like this There we go. And now let's go ahead inside of our editor.tsx components. So where we actually render the React flow. And what we have to do now is we have to find a way to well you know initialize that editor instance So what I going to do here is I going to do const set editor instance Actually, let's just do set editor. Use set atom from your type. And let's pass in editor atom from store atoms, which we've just created. So we are working inside of features editor. components editor.tsx and we've created a neighboring store atoms. So that's why I was able to import the editor atom from . . forward slash store atoms. Now that we have this, what we can do is very simply pass a new prop here in the React flow called on init and pass in set editor. As simple as that. So now the moment editor is initialized, it will initialize the React flow instance inside of this atom. And now what we should do here is we should load that inside of the editor header. So let's go inside of editor header.tsx right here. and in here as you can see we have a completely unused save button here it is editor save button so what we have to do inside of the editor save button is load that instance so const editor will be use atom value from yotai editor store my apologies editor atom from store atoms and now we can pass the value around so let's also go ahead and do update workflow let's call it save workflow to be use update workflow make sure you didn't import the name one so we are using the new one we just created which uses trpc workflows update which has all of those big inputs. And now let's go ahead and do const handle save. We're going to do the following. So if there is no editor, let's just break the function, right? No point in doing anything further. And now let's get the nodes by using editor.getNodes. Then let's get the edges by using editor get edges and finally let's do update workflow dot mutate pass in the id to be workflow id and pass in the nodes and the edges and my apologies this is save workflow dot mutate and as you can see no errors because the type of nodes that i receive here is fully compatible with the type of trpc workflow input that we've created perfect now that we have that let's go ahead and change this to be handle save and let's go ahead and change this to be our save workflow is bending there we go let's see i'm using handle save i think this should all be working now so let's go ahead and try it out. So I think it's best that we just start a new workflow completely. So like a clean working slate here. And I'm going to add a manual trigger. And I'm going to add an HTTP request. And I'm going to connect the two. And I'm going to position them in some way I can recognize like this. And I'm going to click Save. And let's see. Workflow, then the name of the Workflow saved. Now I'm going to go back inside of my workflows list. I'm going to refresh here. So I clear any cache or any, you know, store. And once I load this, there we go. We can now officially save our editor state. And we didn't even have to do anything special to load it because that's already finished, right? Instead of editor.tsx, we already do that. When we load it here using the use suspense workflow, we already populate the nodes and the edges from it. So we are now successfully transforming between server values for connections and nodes and React flow values for nodes and edges. They are one and the same now. So that's one part of our job finished. Let's go ahead and mark it as finished. So we added the save functionality. now let's go ahead and implement the delete functionality now this one shouldn't be too hard either so what i want to do is go to the place where we actually call these functions so i believe that is currently inside of components i think i have uh it's not instead of features so it's it's we have to modify two components one is for the trigger and one is for the execution let's go inside the trigger one first and let's find the base trigger node which we developed last time And in here, we have handled the lead, but we never really implemented it. So let's go ahead and do that now. So I'm pretty sure there is an easy way to do that by using set nodes and set edges from use React flow. So as long as this component is initialized within the editor, which I think is definitely the case, we should be able to modify set nodes and edges from here directly. We have the ID, which is great because we need the ID to be able to manipulate and filter this node. So inside of handle delete, let's go ahead and try set nodes and get the nodes. And now what we need to do here is let's call this current nodes so it's easier to understand. I'm going to open a function like this and I will do const updated nodes to be current nodes dot filter, get the single node and compare that nodes ID against the ID of the node here. So if it is not ID and let's simply return updated nodes. There we go. But we also have to delete all the edges. So let's go ahead and do set edges, current edges here. And I'm going to do the same thing. Const updated edges are going to be current edges dot filter. Get the individual edge and check if edge, whoops, edge.source is not equal to node to ID and if edge.target is not equal to ID and return updated edges. And I think that this should now be enough for us to, well, handle delete. So we already have handle delete here. So that seems to be working. Now this is just for the trigger node. So the only way you can test this is by this. I'm not even sure if we connected the button. Let me try and click here. It looks like it works. And if I click save, looks like that works. And if I refresh, that works too. And don't worry about the position change. The node didn't change its position. We just always do this, right? Every time the editor loads, we focus on it. Perfect. So that seems to be working. That was very simple, wasn't it? let's go ahead and copy this entire implementation here and let's copy it inside of the base execution node and that will be the only other place where we are going to need this i think so uh instead of executions uh base execution node and let's do the same thing so let me remove this paste all of this fix the indentation and import use react flow from xy flow react and now this should work as well so if I go ahead and select this and delete it there we go works amazing obviously if you don't save and refresh it will still be here perfect so that's one more issued taking care of let's go ahead and mark that as finished as well but now we have the settings functionality and this one is a little bit more complicated but I do want to work on it because it's the only way we can move forward by actually executing these things. Just before we do that, there is one more thing that I want to show you and I want to confirm if it works or not because of the changes we just did. It is inside of features, editor components, editor.tsx. So it's quite annoying that right now we can only delete one node at one time, right? So if I have like two nodes here, I can't select them. Maybe I can, I don't know, but I'm not really seeing that indicator quite well. So let's go ahead and add some more props to React Flow in order to make it better. So I'm going to give this snap grid 10 and 10 inside of an array like this. And now it's kind of having to follow the grid. So if you snap it around now, let's also do snap to grid. and when you try moving it around now you will see how it kind of sticks right i think this is a slightly better experience now simply because it can't go anywhere so you will be able to center them because of the snap to grid then let's do pan on scroll now pan on scroll will basically change the controls here they are going to work on the opposite directions this will make it much easier to work with this with a touch bar and if you enable pan on scroll basically it's just the way you modify your canvas it will change the controls and it's actually more familiar to what's appier and n8n use in my experience and let's do pan on drag and let's go ahead and actually disable it and instead let's add selection on drag so what will happen now is that you can see you can now only move this by using your scroll mouse right but or if you're in a touch bar it's much easier actually but what you can do now is this you can now finally select them okay and I think that's actually all I wanted to do but now that you actually are able to select your nodes here you can go ahead and delete them there we go I just wanted to test if that is working. So yes, we just added this three things here. If for whatever reason you don't like that behavior,\nyou can remove it right so i tried to replicate the behavior of popular platforms and this is mostly how they work uh the thing that's hard with this is moving the canvas around right uh i haven't really found uh any other way to do this other than using scroll bars now like you can't drag because that is selecting uh if you have a laptop touch bar then you can easily do it. The way I'm able to scroll left and right using my mouse is because I use a mouse that has two scroll bars. So I don't know how you will be able to do it. So for that reason, you can play around with this settings and see if you like them or not, and then just remove or leave them. It doesn't matter, right? I just wanted to show you that there is a way to select things and then delete them using the backspace. All right. So once we have this, let's go ahead and let's work on implementing the settings. So let's start with super simple one by going inside of features, triggers, manual trigger node.tsx. The reason I say super simple is because the settings for manual trigger literally don't even exist, but we are going to create them anyway, simply to be consistent with user experience of all of our nodes. So instead of manual trigger, create a new file called dialog.tsx. Mark it as use client and then import everything we need from the dialog components which come from chat CNUI. After that, go ahead and create props like this. and let's go ahead actually we can just do like interface props export const manual trigger dialogue assign the props here and execute like this then get the open on open change here and now all we have to do is just create the dialogue so that's fairly easy to do We render the dialog, we give it open, and we give it on open change, on open change, and then we render the content. Inside, we render the header. Then we render the title, which will say manual trigger. We go ahead and copy this and render the description. inside the description here well we can just say configure the settings for manual trigger node and the dialog header create a div with a class name of py4 inside a paragraph manual trigger because that's the only thing we can really say to the user now right this is a manual trigger what else can we say? Obviously, I'm doing this to show you how this will look like in a more complicated node. Now that we have this dialog, we can go back inside of this node and then we can finally make sense of this fragment by also adding manual trigger dialog and let's render it above. I think that's semantically correct. Manual trigger dialog, did I forget something? Why is it not? why is this an error oh it's missing properties oh yeah okay so now let's go ahead and just add a simple state here from react dialog open set dialog open and all we have to do is just pass those two props and that's it that's all we need so this is a super simple example because there is no really any settings, but at least we have the UI for those settings. And now we can enable these and handle open settings can just be a super simple function like this. The reason I'm separating it in a function because in other components, this can be obviously more complicated. So let's go ahead and just get a new workflow going on here. and go ahead and add a trigger node and now when you click on the settings button it will open it manual trigger configure settings for the manual trigger node now in here i've just left this small comment and you can also do double click on it by the way yeah i've left this small comment like manual trigger but in some other node this wouldn't be as simple this would be a whole form here. So for example, I don't know, in order to make this better, maybe you can say, use to manually execute a workflow. No configuration available. I don't know, something like that. But I think it's a good idea to have it here. Simply so, users always expect to see more information about a node when they double click or when they click on the settings. So they will try that on like other nodes here, because in here, obviously we need it. Like what HTTP request, what method, any authorization keys, right? That's what users will expect. So we have to teach our users that they can always access the settings to see more information. Great. Now, while we are here, I actually have an idea of also enabling the status, because I told you we can't do the status, but we actually can. And it's not that difficult. And if we do it now, we won't have to remind ourselves to do it later. And it's not too difficult to do. I think that the number one thing we have to do is install a package from React Flow. So let's go back here. Let's go instead of UI. And here we have it, the status indicator. So this is it, right? And let's go ahead and do this. So npm here. Oh, you also have manual here. Yeah, you can also just copy it if you don't want it to be added. Sure. Or you can use, you know, my public assets folder. And in there you can find all of these components that I added this way. So npx chat cn. Okay. Base handle. Change this to node status indicator. Okay. and then immediately after I get this, I'm going to move it into its own folder. So instead of components, here it is, node status indicator. Let me just confirm it's been finished. It is. I'm going to drag it and I'm going to drop it inside of React Flow so I know all of those have been added by them and not by me. All right, now let's go ahead and do the following. We have to go inside of base trigger node. So let's go inside of base trigger node here. And here we have a to-do wrap within node status indicator. So let's do that. I'm going to add node status indicator. And I'm going to wrap the entire base node in it. And I'm going to invent it like this. Now in here, let's go ahead and give it a status. which for now can just be set to initial. Let's give it a variant of border and let's give it a class name of roundedL2Excel. It looks like it does not accept class name. All right, so that's something we're going to have to modify the node status indicator for. But let's go ahead and just take a look at our app now. So nothing is happening, but if I change the status here to loading, there we go. you can see something is happening. If I do success, you can see it's green. If I do error, you can see it's red. So what we have to do, obviously, is make it match the look of our current node. So first things first, let's make sure loading is turned on. I think that one might be the most complicated one to match. So now we're going to go inside of node status indicator, instead of the React flow folder. And we're going to modify it so that it works for our use case. So first things first, let's go all the way to the top here instead of the node status indicator props. And let's add class name to be an optional string like this. Then inside of let's see. So this is a spinner loading indicator. That's fine. Let's go inside of a border loading indicator here. and let me add class name here and let me add class name here and it's going to be a optional type of string there we go okay so we have that and now we have to find a way to render it so i'm just thinking what is the best way to render it i think it should be in this class name specifically so let's go ahead and open this instead of curly brackets like so and let's import cn looks like we already have it in use here. Perfect. So I can just drop this as the first value and then extend it with class name. As simple as that. Perfect. I think that for now, yes, let's also do it in one more place, which is this one, node status indicator. So this is one that actually has to pass the class name further down. Okay, so we have the loading, we have the overlay, we're not interested in that the border is the one we are interested in so let's pass in the class name here class name there we go you can see no errors perfect for the success one for the other one let's not modify anything now let's just go back instead of the base trigger node and let's try and changing the class name now there we go no more type errors and when i click save looks much better, doesn't it? Try changing this to error. Not good. Okay, something seems broken. Success. Oh, yeah, okay. So head back to node status indicator. We have to modify the success and the error ones very simply by adding cn here. Like this, so cn, and then pass class name as the second argument. and same thing here cn rapid and pass class name as the second argument perfect there we go now let go ahead and see if we can slightly modify this So for the error I want to use border red 700 with 50 opacity For success, I want to use emerald. I want to use green 700 with 50% opacity. So exactly like this, yes. All right. Now let's go ahead inside of the status border here. and let me see if I can slightly modify this. So I'm going to modify minus left to be 2 pixels, minus top to be 2 pixels and I will do this calculation to be plus 4 pixels and the width calculation will be plus 4 pixels as well. So this will make the border look thicker. For a rounded, I'm just going to change this to rounded MD and I'm going to change the border to 3 And now it will just be like a thicker border. All right. Now let's go ahead up here and see what can we change here for this spinner. Let's see. We have rounded seven pixels. Let's change this from rounded seven pixels to rounded SM. And let's change this to loading so we can see what we're doing. rounded SM. All right. And let me go ahead and change the colors a bit. So this will be, we're going to turn this into RGBA. And I'm going to pass 0.5 here. And the second one can stay the same. Yes, I don't think it needs any modification. But we will be modifying something here above. right here. So we're going to make this thicker as well. So minus left two pixels, minus top two pixels, height calculation will be four pixels and width calculation will be four pixels as well. So now all of these have thicker values. I mean thicker look. Okay. Obviously this is just my personal decision. You can, of course, modify this however you prefer. Now, let's go ahead and let's just use a better way of passing the class name. I mean, passing the status. So right now, instead of base node, my apologies, not base node, instead of features, triggers, base trigger node. We just passed the status as loading. So instead, what I want to do is I want to use the type from our node status indicator component. So I think that we can now do this. I commented this out inside of the base trigger node props. Let me see. Yes, I can now import node status from my node status indicator. There we go. And now that I have the status, I can actually use it here, status, and by default, we are going to set it to initial. And now we can just pass the status here. There we go. So right now, by default, nothing is happening. Now we're not yet finished here, because we also have to now modify the base node. Now that we can accept the status. So let's just do status here status like this. And that will obviously throw an error, because base node itself does not accept the status. So just make sure you have that opened inside of source app components react flow base node. So this was added here using the CLI, I will go back inside of my base trigger node here to make sure this error is resolved. And I will also find manual trigger node.tsx because in here, this is also the place where we actually find the node status. So what I'm going to do is just const node status. And in here, I'm just going to modify it. So now I can send this this way. There we go. It's now controlled directly from manual trigger node instead of from the reusable components. Perfect. And now here we're going to change it as needed. So what we have to focus on now is the base node. So let's go ahead and do that. We're going to start by creating an interface base node props extends HTML attributes, HTML div element, and passing the optional status to be a type of node status from node status indicator component. Perfect. So now we have the base node props, which accept the status prop. And in the forward drive, whoops, we now have to modify this slightly. So instead of just HTML attributes, we can now use base node props, which will extend both the HTML attributes as well as our status, which we should be able to pass here now. And you can see immediately in the base trigger node, we now no longer get the error for passing that status here. So I think that now we can close the base trigger node and just focus on developing the base node. Perfect. So some things I want to modify in here. I want to remove all of these comments. They're just confusing. And I want to remove all of these React flow features here. And now let's go ahead and do this. So I'm going to change this from rounded medium to rounded small. That's the first thing I'm going to do. And immediately it looks a little bit better because the border is now matching. Then I'm going to change this to be border and I'm going to do border-muted-foreground. Immediately a little bit darker now as you can see. I can leave the BG card as it is and I can leave the text card foreground and let me add hover BG accent like this and I can remove this hovering one. We don't need it. There we go. So that is the behavior I am expecting here. And now we have to do something in regards to the status that we are passing. So let's go ahead and do a check. If status is equal to error, in that case, we're going to render X circle icon from Lucid React. And we also have to extract the status from here. Is that not how I do it? Let's see. Status. what am I doing incorrectly here status status is here three dots expected not sure let me just debug for a second oh oh my apologies I didn't realize this is a self-closing tag change this it's no longer a self-closing tag and now you have to make sure to add props.children here like that and then let's do status equals to error my apologies I wasn't aware that it was that it was a self-closing tag and now we can do x circle icon render in here and let's go inside of manual trick oh it's already open here and let's go ahead and just change this from node status to loading to error. And there we go. We can now see a very ugly x circle icon. In order to make it pretty, let's add a class name, absolute. And let's add right, 0.5. Let's do bottom, 0.5. Let's do size 2. Text red 700 and stroke 3. There we go. let's copy this and change the error to success change the icon to be check circle to icon all icons are from lucid react let's go inside of node change this to success and let's change this to be green 700 and the last one will be if the status is loading so let me change this to loading and now this will be a loader to icon from lucid react and let's see so absolute minus white 0.5 minus bottom 0.5 size to this will be text blue 700 stroke 3 and animate spin all right so now I believe there is a slight problem here because you can see how the spin, when we add the spin animation, it kind of moves further here. So let me try and doing right one. Actually, let's see. What if I do minus right 0.5 and minus bottom 0.5? I think now it's exactly positioned as, for example, error. Yes, that is the exact position. Perfect. So we can now bring this back to initial and now we have our look and feel as we expected it. Amazing. Now the only problem here is, let's remove the trailing comma. Now the only problem here is that this currently does not work if you go inside of the HTTP request node. Inside of executions, components, HTTP request node. So if I go ahead and do const node status and set it to loading. and if I try to pass the status here, you can see that node status will not even be accepted here. So now we just have to do the same thing to the base execution node as we did to the base trigger node. So there we go. We can uncomment this inside of base execution node props, import the node status from here and also import node status indicator component itself. Let's see, do I have a to do? I do. So wrap within node status indicator. Here we go. So the entire base node is now wrapped in that. Let's go ahead and let's pass the status to be the status prop. Let me just see if I actually use it I don So here I have to use it after I added it as a type of course And I also have to give it a default value of initial in case it not passed You can see how immediately this starts working. Let's also give it here. So status, status to the base node. And you can see how we now just reuse that entire code for this specific thing. And let me just check if there is something that we've missed. I think we just have to specify variant border here just in case. There we go. Let's go ahead and set up the node in the HTTP request. Change this to error. Works. Success. Works. Amazing. Perfect. So we just fixed all of the to-do things when it comes to our status which I think it was very easy to demonstrate. I thought there was going to be a problem with demonstrating so for that reason I didn't want to do it but looks like it works just fine and now that we added the settings let's wrap this chapter up by implementing the settings for the HTTP request so you can see how we are going to preserve some actual node data because this is super simple right just a manual trigger but what about this one this one is more complex this one requires some kind of form right now let's go ahead and focus on that let me see is there something i can check off from here so we added uh well we almost finished the settings functionality i don't want to do it i don't want to check it just yet so let's focus on that now so let's start by copying the existing dialog inside of source features triggers manual trigger and copy the dialog and paste it in the http request folder now let's go inside of that dialog and let's rename it so instead of manual trigger dialog this is going to be http request dialog and let's change this to be http request like this and let's change the description to be configured the settings for the HTTP request node. And inside of here, let's remove this entire div, just leave an empty space for the form that is about to be here. Now let's go inside of the node of the HTTP request here. And let's go ahead and add a simple state control here. so I'm going to do that right here. Basically a simple use state from React for dialog open and set dialog open with a default value of false and I'm going to move the node status here simply because I want it to be consistent to my manual trigger and that comes right after this. It's easier for you if you make your code consistent. It will be easier to understand what's going on. At least that's the case for me. All right. So now that we have that, let's go ahead and do const handle open settings, set dialog open to true. Great. and now we can go ahead and we can render HTTP request dialog from dot slash dialog and we can go ahead and pass those two values open and on open change. There we go. Perfect and now let's go ahead and pass in the on settings and on double click to be handle open settings and now it should behave exactly the same. So if I click here, there we go. HTTP request, same as this. If I double click, it opens exactly the same way. Perfect. So now we have to configure our HTTP request dialog to accept some more elements here. So instead of this dialog, let's go ahead and extend the props. so I'm going to add on submit here to be an optional of actually it's going to be required so it will accept values which are going to infer type of form schema which we don't have yet but we will in a second and then we're going to add a default endpoint to be a type of string default method to be a type of get or post or put or patch or delete and default body will be a type of string. Now let's go ahead and let's import z from zod and let's do const form schema z dot object endpoint z.url and an error message please enter a valid url. Method is going to be z.ennium with options of get, post, put, patch and delete. the body will be a string. It's going to be optional. Not every HTTP request needs a body, right? It can just be a simple get method. And for now, I'm going to leave it like this. I will prepare a refine and I will comment it out. We will need refine later, but it makes no sense for me to do that now because it will be hard to explain why. So I'm just leaving this as a to-do not forget that I have to do that. It's basically to accept a specific template language that we are going to implement later on. All right. So now we have all of those. We have default endpoint, default method, and default body. And now our HTTP request is actually expecting that. So after on OpenChange, let's do onSubmit here. For now, just this. Let's do default endpoint to be node data dot endpoint like this. Default method to be node data dot method. Default body to be node data dot body. And now when I'm writing this, I'm realizing that I could have technically just sent initial values as the node data object. And I think that is a much better solution actually, especially when it comes to extending this in the future. So for the next chapter, I will make sure to look into improving this. I will add a little comment to do check if it can be improved by just sending initial values node data because this is kind of pointless writing every single thing here and it will save time in defining this as well all right now let me just check something so props.data always exist okay let's just go ahead and do this and I think that's okay yeah no errors perfect. But still, nothing really happens here. So now we have to develop the actual form. So in order to do that, let's go ahead and let's import everything we will need to create that form. So that will include all the form elements, form, form control, description field, form item label, and form message. We're going to need the input from components UI input. We're going to need select from components UI select and we're going to need text area from components UI text area. Now for some other components like Zod we're going to need the Zod resolver from hook form resolvers Zod. We already have this installed and we used it already. Alright, good. and we're going to need use form from react hook form and we're going to need use effect from react to update if any new values are received in the meantime. All right, I think that's all for now. Later, we are going to need, I'm just going to add JSON 5, simply like a reference for me so I know what I have to explain here. But for now, we don't have to concern ourselves with that. Now inside of the HTTP request dialog it would be a good idea to extract all of those new values here. On submit default endpoint default method and default body I assigned this as a default value to get. Again this might change later I will explore for the next chapter to see if we can improve this by sending just the initial values. Let's do form use form and inside of here Add default values here. Endpoint. Default endpoint. Method. Default method. Body. Default body. Like this. Now let's go ahead and improve the type safety of the form by adding z.infer type of form schema. And let's also add resolver here. ZodResolver form schema. There we go. Now, let's go ahead and do this. So const watch method will be form.watch method. The reason we are doing this is so that we can do some dynamic field hide and show depending on the method that the user selects. So basically, we are going to show the body field if either post or put or patch are in the watch method, which we are looking at above. In the handle submit, let's define the values as z.infer type of form schema. Let's call on submit. and on open change to false so we close the model all right now we are ready to actually build the form So let add a form and let spread the form in it Now let use a normal HTML form element And let's give it an on submit prop of form dot handle submit handle submit inside. so the same way we built the form for the out screens space y8 margin top 4 the reason i'm comparing this to out screens is because i won't be doing as in-depth explanation as i did during that part where it was the first time we encountered building forms right so let's now add form field which is a self-closing tag let's give it control form.control the first field will be used to control the method of this HTTP request. Let's go ahead and execute render here like this and it's going to render form item with a form label method and then we're going to add select. Let's give this select on a value change field on change default value field dot value and then let's open form control select trigger let's give it a class name full width and let's give it a value which is a self-closing tag with a placeholder select a method all right and now outside of the trigger outside of form control we're going to add select content with all the possible options so let me expand my screen select content with multiple select items with matching value here and the value prop this value prop is very important so make sure you don't misspell it. Basically add all the options that we write here and here in the enum. And now we have our first field. We can now select basically what is this method's mode of request. And now let's also add a form description here, the HTTP method to use for this request. And let's render form message, which is a self-closing tag, which will appear if there is an error in this field specifically. Perfect. So now let's go ahead and let's copy this entire form field. Let's paste it here. And now let's go ahead and modify it. So this one will be used for the endpoint. So let's go ahead and change the form label to endpoint URL. And luckily this will be a little bit simpler. You can remove the entire select thing and after the label add form control render the input which is a self-closing tag spread all the field properties and just give it a placeholder here and this is the one I'm going to use simply because it will be a cool way of showing your users what you can do. For example, you can query api.example.com users and then you can actually do like a dynamic thing here. Of course, you could manually write one, two, three, but if you want to reuse the data from the previous node, you could use our templating language. Now this makes no sense to you right now, but this is what we are going to implement. We will basically allow users to reuse the data from previous nodes. Right now it makes no sense because we just have this combination, but imagine the HTTP request was connecting to some third node which already has some data in it. So that's how we're going to be able to do that. What if it was three HTTP requests connected one after another, right? That's kind of what I'm trying to show here. And because of that, we're going to add a advanced form description here like this. Static URL or use, and then I'm using this kind of thing so I can display double brackets, like use variables for simple values or use JSON and then the name of the variable to stringify objects because you will be able to pass objects, you will be able to pass individual values. So this is just some instructions for the users how they will be able to use this. Don't worry, this will make more sense later once we actually do this, which I commented out, JSON 5, refine, you will see how cool it is. For now, let's just write the explanation for it. And one more field left, which is only if show body field is enabled. So only then are we going to render this form field. So let's go ahead and just copy the render for this one, paste it here. Let's go ahead and give this control form dot control. Let's go ahead and give it a name of body. There we go. So this will be the request body. Instead of using an input, it's going to be using a text area. and let's give it a class name minimum height of 120 pixels font mono and text small so now when you switch to post you should see a request body in here because it only shows if either post put or patch are selected because otherwise you don't need the body field and now what I'm going to do is I'm just going to improve the placeholder for this so the placeholder will be a little specific you don't obviously don't have to do it the same as me so this will be the placeholder just explaining to the user that they can do like dynamic values in here so this is how it looks like in here you can pass an object define user id name and items and you can use the dynamic templates language here to show variables, right? So I'm going to update the form description to kind of resemble that as well. I mean, it already kind of resembles it, but let's go ahead and do this. JSON with template variables. You can use variables for simple values or JSON variable to stringify objects. and basically you are now explaining to your users like hey you can do advanced things here you can use the value from your previous node and add it here so obviously this is just placeholder it's not important code so you can just write whatever you want here if you can't bother with you know writing this it's obviously a long and annoying line of code but we will implement this later. And in order to wrap this up, let's just end it by adding a dialog footer, which we forgot to import, and button, which we also forgot to import. Make sure it has a type submit save, and this a margin top of four. There we go. Perfect. So now what we have to do to end the chapter is find a way to preserve the value. So let's go back inside of the node here and let's actually do that. So what we're going to do here is we are going to add set nodes from use react flow like this from xyflow react and then I'm going to go ahead here and do const handle submit. The values which I'm going to accept are endpoint which is a string, method which is a string as well, and body, which is an optional string. And then in here, I'm going to do set nodes, and then I'm going to go through my current nodes, and I will do nodes.map, find the individual node, and I'm going to find the node which matches the prompts.id, which means, okay, I found the node that I have to add this data to and let's simply return by spreading the existing value of the node, opening the data object, preserving whatever is currently in that data and then just adding those new values like this. And then I also have to do return node here. There we go. So very simple handle submit. I will take a look if we can somehow reuse the, well, we can definitely, you know, reuse the values from the dialog. We can just export a Z infer type of form schema. And then we can use that here. So it doesn't have to be like this. All right. And now that we have the handle submit, let's just go ahead and pass in the handle submit here. There we go. I get no errors simply because my types are exactly the same here. But we can improve this definitely. Let me just go ahead and do export type. I don't know, form type. And then I can import this here from .slash dialog. There we go. and now the values have the exact same thing and I don't have to manually type that perfect so now on submit will go through the state of the react flow and it will save those values there is just one more thing we have to do before we test this out and that is instead of dialogue instead of here we need to add a use effect\nto reset the form values if the dialog opens with new defaults. So let's add use effect like this. If open form reset endpoint default endpoint method default method body default body like this. and let's go ahead and add open default endpoint default method default body and form all right and i think that now this should be working fine so let's just do a refresh here and let's add an HTTP request. Let me select a post here. Let me do HTTPS codewithantonio.com and in here I'm going to do, I don't know, we're just pretending here, user ID 123. And let's click save. And you can see how immediately we have post method targeting codewithantonio.com. Perfect. So if I go inside of my node.tsx, you can see that that's exactly what happens here. We are reading the node data and we can now construct the description here. Let me just quickly go inside of base execution node. I think I made a typo here for truncating Let me see Oh workflow node Let go inside of workflow node So that is located inside of source components workflow node Am I missing, yes, truncate. I just had to fix that typo. There we go. So now it's all in one line. Perfect. And you can see when I open, it's preserved. And if I click save here, and if I do a hard refresh here, there we go. It is all preserved. The data is saved successfully. Amazing, amazing job. So we can now mark that as completed. Amazing. That's it for this chapter. So 17 editor state. We have 18 files. Let me close this. So these are all of my files. 18, 17 editor state. Let me go ahead and open a new branch. 17 editor state I'm going to stage all of my changes here 17 editor state I'm going to commit I'm going to publish the branch and then I'm going to go ahead and open a pull request to review all of these changes and see if there are some improvements or critical mistakes that we've made And here we have the summary by CodeRabbit. So new features. We added visual status indicators for nodes, loading, success, and error. HTTP request node is now configurable via dialog. We can configure the method endpoint and the body A manual trigger node now also includes a setting dialog for consistency Add node button opens the node selector So this actually worked before but we now explicitly gave it an on click. So it's no longer an empty on click as per CodeRabbit's previous comments. Editor gains save action for workflows with feedback and data persistence. Exactly. And we changed the editor interactions with grid snapping, scroll to pan and refine selection drag behavior. obviously I told you to adjust this to whatever you like that you can work with. And we also added functionality to delete the nodes and its edges. And another bug fix to fix the text truncation that at least I had. Now I don't think there was anything like too complicated happening here but we can definitely take a look at how our save function works. So once we save using the editor header. We read the editor atom, which was initialized when it read the editor instance, and we send a mutation with nodes and edges. And now what we do is we have a transaction, which will verify the workflow ownership, delete existing nodes, and also cascade all connections, and then create new nodes and new node data according to that. Same goes for the connections, and finally it will update the workflow and that will result in a success message for the user. In here we have a super simple dialog explaining how we populate the values of the HTTP request dialog and how we update it using React flow set nodes As per some comments here we have some TypeScript issues it appears but I think this is incorrect It telling me Border 3 is not a Tailwind utility That because most AI models are not familiar with Tailwind version 4 which I pretty sure introduced Border 3 In here, it's telling me to not use the default value and instead to use value, which could be a good point. I'm not sure. I will take a look at that. and in here yes it's basically it doesn't like the fact that we are casting the value here so for the type I just defined the string so in here it's telling me to use a native enum and pass the node type we could do that but then on the client I would have to do casting so we'll see I will take a look if there is a better solution for this. Other than that, all good. So not as bad as I thought it would be. We did write a lot of code, but looks like most of it was good. Let's go back to the main branch. And as always, let's make sure to synchronize our changes. Once you synchronize your changes, I always recommend just going inside of the graph so you can see that everything here is fine. Perfect. So now that we've done that let's go ahead and wrap this chapter up by confirming that we pushed to GitHub, we created a new branch, new PR and we reviewed and merged. Amazing, amazing job. You just finished with the editor state and see you in the next chapter. Thank you.",
  "transcript_chars": 530565,
  "transcript_filled_at": "2026-06-06T16:20:45.447907+00:00",
  "transcript_filled_by": "tk-bulk-groq-retry-20260606"
}