How To Build A Production Ready API - Auth, JWT, API Keys
- Views
- 38.9K
- 38,918
- Outlier
- ×0.8
- Median 47.5K
- Length
- 1:44:22
- Published
- May 26
- Tuesday
- Comments
- 138
- 1.4K likes
Script
How it's written
- Words
- 24,797
- Words a minute
- 238
- Reading time
- 105 min
- Segments
- 75
The hook
0:00–0:26In this video, I'm going to show you how to set up a fully type-safe Hono API that's production ready. And we're going to be covering things like every type of authentication you would need. For example, we have email password-based authentication. Also, when you log in, it's going to be based on JSON web tokens, which can then be used to create various different API keys. And these API keys are all being hashed and secured properly inside of the database we're going to be using inside this project. And then we have basic create, read, update that have various different
Structure
In this video, I'm going to show you how to set up a fully type-safe Hono API that's production ready. And we're going to be covering things like every type of authentication you would need. For example, we have email password-based authentication. Also, when you log in, it's going to be based on JSON web tokens, which can then be used to create various different API keys. And these API keys are all being hashed and secured properly inside of the database we're going to be using inside this project. And then we have basic create, read, update that have various different permissions for what you can do based on if you're logged in or not and based on your different admin or non-admin levels. This is going to give you that baseline of everything you need to know to work with Hono inside of an API, and it's all going to be type-safe and production ready. And if you're curious what this amazing tool is that lets us do all this API testing fully automated and incredibly easily, this is the Requestly API client. It's an entirely free API testing tool that makes testing local or production or even staging APIs incredibly easy. They were kind enough to sponsor this video. And if you're familiar with a tool like Postman, Requestly is essentially a very similar tool, but it cuts out a lot of the extra bloat and things that annoy me personally about Postman, and it's just a better alternative. And again, it's entirely free. There'll be a link down in the description if you want to check them out. Welcome back to Web Dev Simplified. My name's Kyle, and my job is to simplify the web for you. The very first thing we need to do to get started is just to create a Hono application. So, we can type in npm create hono@latest.
Watch from 0:00And if we just put a period afterwards, that'll create it in our current folder. And this will allow us to create a Hono app. Now, for the templates, it really doesn't matter too much which one you choose. This kind of depends on where you want to deploy. In our case, we're just going to be choosing Node.js cuz that's like the most basic one that there is available. And we'll say that we do want to install the dependencies using npm. And it's going to clone down just a very basic starting code. As you can see, there's really not much. If you look inside of our source, you can see we just have a very simple file right here that just starts up our server and serves that content. And the only thing that's being served right now is this route that is the home page. And it just gives us the text of hello Hono. And we have a basic TypeScript config because again, all of this is going to be purely type safe. In our package.json, you can see there's pretty much nothing in here other than a dev script to start up our application. So, let's run that by saying npm run dev, and that's going to start up our application. You can see here server is running on this localhost:3000. And if we just open that up, you can see it just says the text Hello Hono. It's quite small and difficult to read. There we go. You can see that text Hello Hono. Now, what we need to do is obviously turn this into an API. We don't want to be returning text inside of this. We want to be returning JSON and so on. So, the next step is to turn this into an API. And the first step I really think I want to work on is just to be able to work on the most basic fetching of different data, and then we can move on to adding in our database and authorization. Now, inside of Hono to create different routes, you can say app.get, or we can do app.post, delete, put, and so on.
Watch from 1:23But, we generally want to structure these routes inside of different folders to make them easier to work with because if we put all the routes for our entire application in this one file, it's going to become quite large and unwieldy. So, instead, I'm going to create a brand new folder. This folder is going to be for all my routes. Let us call this routes. And inside of here, we'll create a folder for all of our author-related routes. We're just going to call this author.ts, just like that. And now, all of our author routes, for example, this route right here, are going to go directly inside this folder. And to create nested routes, all we need to do is just generate a new Hono app. So, we can say const app equals new Hono, just like that. And if we properly import Hono, now we have the entire Hono app working, and this is just a nested route. So, now what we can do inside of here is instead of saying app.get, we can say app.route. We can pass it along what the path is going to be. So, for example, {slash}author. In our case, let's make it {slash}authors, just like that. And then we can import the different routes we want to use. So, these are going to be our author routes. So, we want to make sure we export those here. So, we're just going to say we're going to export our app, and we'll set that as a default export, just like that. So, now I can import that author routes up here by just saying import author routes from and I want to get inside that routes folder /author just like that.
Watch from 2:42That's going to import those routes. And if I just make sure to put that .ts extension at the end of that, you'll see it gives us a really small error. This is something we can fix in our TypeScript config quite easily. We can just come in here and use this rewrite relative import extensions, set that equal to true, make sure I just wrap this in quotes, and all that's going to do is it's going to take any place that we have this .ts extension and convert it to .js for us. So now everything is working like we expected it to. And now we have this /authors route, which is just rendering out this text. We'll just say authors. So now if we go to Hello World and we go to the /authors route specifically, you can see it prints that out. And the nice thing is is we can nest this. So this is /authors, and if we want to make this route slightly differently, for example, we want to say authors two just like that. Now you need to go to /authors/two since they're going to stack on top of each other, and you can see it prints out that exact text just like before. Now before we start hooking this up to a database, I just want to have some placeholder data that we can use. So we can say const authors is equal to an array, and inside of here we're just going to put in some random IDs, and we'll just give them a name. So for example, Kyle, and we'll copy this so we have two authors in this section. Just like that. We'll have Sally, and this one will be an ID of one and an ID of two. And now I want to create all of my different create, read, update, and delete routes. So the very first route that you're going to have instead of a normal REST style API is going to be your list route. This is going to list all the different things that the user has access to. So we want to return all of these authors. And we're creating a JSON API, so we want to return this data as JSON and not as text. So here with that C value, which is all of our context for Hono, we can just call .json
Watch from 3:57and pass it along any JSON object we want, and it's going to return that data as JSON to our user. So now we've created a list route that's going to return all of that data as JSON. Now again, to just really quickly test that, we can open up that server, we can go to /authors, and make sure I spell that properly. There we go. And you can see we've got printed out an array of all of that different data as JSON just like we expected it to be. Now, the next thing you're going to create in a CRUD application is the ability to get an individual author from our list. So, instead of just a list, we want to get an individual user. To do that, we can use that same app.get cuz we want to use a get method here, but instead of getting all of them, we want to get specifically an ID. And to get a dynamic parameter, so for for example, I could pass along /1, /2, /3, and so on, we can put a colon in front of whatever our variable name is and then write out our variable name, and this will automatically add type safety for that variable name inside of our parameters. For example, I can say c.request .param, and that's going to give me all of my different parameters. And I specifically want to get just the ID, and you can see here I already get full type safety. So, I'll type in ID just like that. And I can come in here and now I get type safety that this ID is going to be a string value because Hono is smart enough to understand all of this automatically, which is why I prefer Hono over something like Express.
Watch from 5:30Next, what we just need to do is filter our list to get the author with that particular ID. So, what we can do inside of here is we can just say authors Let's actually say we want to get an author, which comes from authors. We want to filter. Actually, we want to find specifically just an individual author where the ID is equal to our ID just like that. And then we can return that author down to our user. And if we didn't have an author, we could for some example throw down an error. So, we could say if our author is equal to null, that means we don't have an author. Instead, we can just return down some type of error. So, let's come in here with an error that says author not found. And we can also throw along a status code of 404. So, the second parameter of this JSON function allows you to change the status code, and the HTTP status code 404 just means not found. So, it's just a little bit more information we're sending down to the user. So, we're either sending them the user or the author in our case or we're sending them down an error. So, again, let's just test that out by opening this up and manually doing some testing. So, we can say {slash} author {slash} one, and we should hopefully get the first author, and it looks like it's not returning that to us. Let me just make sure that all of our code is correct. authors.find, it should be finding it. It is at that {slash} ID, so that should be correct. Let's just refresh this one more time. And actually, the reason this isn't working is it should say authors {slash} one.
Watch from 6:45There we go. So, you can see it's returning the first author. authors two returns the second. And if we pass in an ID that doesn't exist, you can see we get that error author not found, and it's returning the correct error codes and everything else. Now, that's pretty much as far as we can get testing using just our browser. It to do further testing, we're going to need to use the requestly client. So, I'm going to just go ahead and start getting set up on that so we can do the testing for our post request, put request, and delete request, and also make this type of testing much less manual. So, inside of requestly, when you first get set up, you're going to have a completely blank environment like I do. So, the very first thing we want to do is to create a new collection, and this is where we're going to store everything for this application, because if you're working on multiple applications, they're all going to have their own collections. We'll call this library, because that's a relatively self-explanatory name for what we're doing, and that essentially gives us this nice little library folder right here. Now, inside this library folder, I also like to add additional collections. So, we're going to add a collection into here, and this is going to be for all of our author related stuff. So, we'll add the authors into there, and then we can add individual requests for what we want to do. For example, this is going to be for listing out all of our individual authors, so we'll call this list. Now, you see automatically, by default, it shows this get request, cuz we're doing a get. We'll leave it as a get, cuz that's what we want, and we can paste in our URL here, which is just going to be localhost 3000. Make sure I have my slashes up here, and we want to go to {slash} authors.
Watch from 8:06And that's going to get all of our authors when we send along this request. And if we just save that and click send, you can see we get a list of all of our different authors being sent back to us. We can also create a request that's specifically going to be for getting an individual author. So, we'll come in here, we'll create a brand new one. This is going to be called get just like that. This is going to be forgetting an individual element and we kind of want to copy this from our list. So, we'll copy that over, go into our get section just like that. And this is going to be for a particular ID. Let's in our case say the ID is going to be one and then we'll give that a quick save. And if we send that along, you can see we get the user the ID of one and here you can see we get the user with the ID of two. Now, it's generally not ideal to hard code our values up here. So, using that same colon trick, we can say colon ID and that gives us these path variables down here at the bottom where I can specify, for example, one, two, and so on. So, if I put in one, I can send this and get back the data for user number one. This again just helps me a little bit with my testing and I'm going to show you later how we can automate almost all of this to make it incredibly easy. But, the first thing we need to do is actually create the post method to create authors and so on. So, let's just give that a quick save and let's work on our post method to create an author next. I'm just going to copy all of this code so we kind of have a good starting point. I want to do a post and in order to post, generally what you do is you post directly to that main {slash} authors route or {slash} books or whatever it is. So, we're just going to post to that main route just like that. So, we don't have an ID or anything like that. We just want to pass along all the data for the author that we're going to be creating and that comes from the body.
Watch from 9:25Now, we want this to be type safe. So, we're going to be using Zod for all of our validation. So, let's make sure we install Zod as a library dependency. We can say npm i Zod and we're also going to be installing a library from Hono called Hono {slash} standard validator. And this is going to allow us to use Zod to validate these inputs automatically for us so we don't have to manually write that code ourselves, which is really handy. Now, what we can do is we can use this S validate function. There we go, S validator. This S validator is going to take in a string of what we want to validate. In our case, we want to validate what our JSON input is. So, we'll paste in JSON that's coming from our body, the JSON body. And then we need to pass along whatever our schema is going to be. So, we can just call this create author schema or something along those lines. And let's create that create author schema. So, at the very top here, we'll say const create author schema equals z, that comes from Zod, and we want to create a brand new object for all the keys and values we want to test. Now, our schema is going to be very basic. We're going to come in here with a name. This name is going to be a string if I can spell properly. There we go. With a minimum value of one. And we're also going to specify a birthday. Just like that. And what we're going to do is we're going to use z.coerce. That's essentially going to take the type of string, which is what it normally is, and try to convert that into whatever type we specify next, which in our case is going to be a date.
Watch from 10:48And we're going to say that the birthday is entirely optional. There we go. Now, if I give that a quick save, that's our full schema, and that'll give us all of our information for our author down here that we can use. And if we want to, we could even put a birthday on some of these. So, we can come in here. Let's say this one is going to have a birthday. And we'll set that equal to just whatever the current date is, just like that. Now, to get all of that validated JSON data, what we can do is we can say const data is equal to c.request.valid. That's going to ask us what valid data we want. In our case, we want to get the validated JSON data. If we hover over the type for this, you can see that matches the type that we created in our Zod schema. Again, everything is fully type safe, which I really love. Now, the next step is to just save that author inside of our list of authors. So, what we can do is we can just say authors.push. And we want to push in a brand new author. So, we'll say ID is just going to be crypto random UUID. And we can pass along all of our different data, just like that. So, it's essentially creating a brand new author. And we can return what that brand new author is. So, let's actually put it in a new variable. Just like that. We can push that author into our authors list, and then we can return that author down to the user. And we're going to pass along the status code of 201, which just stands for created, saying we created this brand new user. So, now we just gave that a quick save. Let's make sure we test this out. So, let's only bring over that Requestly API client. I want to create a brand new request right here. And this request is going to be specifically for creating, so we'll call it create just like that. It's going to be a post request, and we're going to be
Watch from 12:08going to that URL, which is just our localhost URL. And we specifically want to go to the author section, just like that. And now we want to be able to pass along JSON data. So, we can do that inside of our body. If we specify our body type as JSON, we can now pass along any key value pairs that we want inside of this section. So, let's go ahead and we specify a name for our user. Let's just say that the name is going to be Kyle number two. And we can pass along a birthday if we want. For now, I'm just going to keep it as a name. Now, if we just give a quick send along on that, you can see that we now have a brand new user being created. And this user, for the author, sorry, has an ID being specified, and it has a name. And if we copy this ID, we can actually use this ID inside of our get function. So, down here, pass in that ID. And you can see when we click send, it's now returning us the information for that user, which has an ID and a name. And we never gave them a birthday, which is why no birthday shows up at all. Now, let's just make sure that we save both of these. And now, the next thing we can work on is going to be update and delete. So, let's come in here with a simple update function. We're going to be using put for all of our update methods. So, we'll just say dot put like that. And in order to update, we need to update a specific ID. So, we're going to request a specific ID. Again, we want to validate the information that's being passed in. And this is going to be a brand new schema called update author schema. It's going to look very similar to our create schema. So, we can come in here, we'll copy that down. Really, the only difference is we want to make sure that all of our options are optional, cuz we can sometimes update one thing and not other things. Also, I want to be able to have the option to remove a birthday. So, I'm going to specify this
Watch from 13:38as nullable. This just allows me to pass along null as an option, so I can now remove the birthday if I want it as well. So, that's my update schema completely done. I also want to make sure I can get my ID inside of here. So, we're going to use that exact same ID code. So, now I have my ID, I have my data that I want to update. All I need to do is get that author and update all the information for that user or that author. So, to get our author, that's relatively simple. We can just go through our authors list. We want to find the author where the ID is equal to our ID. And again, if we don't have an author, we'll throw the same error we had before. So, this error just says, "Hey, we don't have an author. Throw a throw a 404 error cuz we can't actually do this." Next, what I want to do is I want to update this author's information so that it's stored properly. So, we can just say that the author.name is going to be set to data.name and the author.birthday is going to be set equal to data.birthday. And since these values could technically be undefined, I'm just going to do a quick check that if our data.name is equal to undefined, I'm sorry, if it's not equal to undefined, then we're going to set it. And I'll do the exact same thing if our data.birthday is not equal to undefined, then we'll set that value properly. And I'm getting a little bit of a type error just because up here, I just need to specify the type for my author. So, we'll say that this author.type is an ID with a string, a name with a string, and it's going to have a birthday that is a date, and it's going to be an optional date.
Watch from 15:09And this is just an array of that data. That should clean up my type errors down here. And actually, birthday I need to make sure it can be a nullable value. So, I'll just come in here with null, just like that. There we go. Now, that does set up everything properly for my author. I'm pushing that back into the list, which I don't even need to do cuz it's already there, and that should update my user and return that user's data back to us. Now, a lot of this is kind of hacky cuz we're just doing this in memory. We're going to be converting over to a database very soon after we get the rest of these methods in place, but for now, let's go ahead and actually set up our requestly for this put method. So, let's bring that over, and inside of here, I want to create a brand new HTTP request. This one is going to be for our update, so we'll call this update. It is a put method, and it's going to go to essentially the exact same URL. We want to make sure it has the ID parameter at the end of it. For now, let's just hardcode the ID of one into there for what we want to update. It's going to take in a JSON body, and we can pass along any key-value pairs that we want. For example, we could say the name here is going to be new Kyle, because we're updating that name to be whatever our new name is supposed to be. And now, if I click send to send along this data, you can see that it's updated that name for this first identity here to be new Kyle instead of what it was in the past. So, with that done, let's just give that a quick save, and now let's move on to the delete method next. So, I'm going to copy all of this code cuz it's going to be very similar. Delete is going to take in an ID, but we don't have any JSON data we're validating, so we can just completely remove that parameter. And these parameters that we are passing along as like the second option here, this is just middleware.
Watch from 16:32So, this is just code that runs before your actual request, which is this final function, and can do different things by adding or changing your parameters. In our case, it's just validating this JSON data for us. So, now down here we have delete where we're getting in our ID, and all I want to do is I just want to remove that ID from our authors list. Now, to do that, we need to find what the index is that we're removing, so we can say index is authors.findindex where that ID is equal to the ID. And if we don't have an index, in our case, if the index is equal to -1, that means it couldn't find anything, so we'll return a not found message here. Otherwise, we're going to go ahead and we're going to remove that from our list, so we can say authors.splice, and we can say that we want to start at whatever our index is, and we want to remove one element. That's going to remove one thing from our authors list. And then, for returning data, we don't really need to return anything because we're just deleting data. We're not adding or creating or updating. So, here, I'm going to pass along a body that is null. That's a completely empty body. And to say that you have successfully executed and you have no data to return, use a 204 status. This is very commonly used with any type of delete that you're doing. Also, since our status up here is not for creation anymore, this is an update status, I'm just going to leave it as a normal 200 status to specify we create or we updated this properly and 204 is being used for our delete status right here. So now let's go ahead and set this up in requestly. Let's come in here. We want to create a brand new request. This is going to be a delete request with a very similar URL except we want to have the ID we're going to be up or deleting. Let's just say we're going to delete ID number one. And now
Watch from 17:58it should be if we send this request along that it's passing us along no data at all. But if we test our get function right here, you can see that ID one has been completely removed from the list. You'll also notice none of our other elements are in the list. That's because every time we save our server, it restarts and refreshes all of our database data. That's because we don't have anything stored in a database yet. But that's the next thing we're going to be working on once we clean up our requestly set up a little bit to make it even more automated than it currently is. So let's just go ahead and save this. Now that we have that saved, let's actually make sure we call it delete just like that. We'll give it another save. And now I want to work on highly optimizing what this is going to do. So the first thing that you'll notice is inside of all of our URLs, we have this HTTP localhost 3000 being copied between every single one of them. This is obviously not ideal. And if we wanted to test our production or staging environment, we would have to manually override this. This is why there's something called environment set up that we can use. So let's create a brand new environment. We're going to call this our dev environment. And we can create any variables we want inside of here. For example, we can create a variable called base URL and we can give it a value. Let's give it an initial value of localhost 3000. This initial value is synced between all of the different people in your workspace. So for example, if you have other people working on this project, this will be synced with them. While the current value is just something local to just you. For our purposes, it doesn't really matter. We'll set the same value for both of them. Then if we give that a quick save, we now have this base URL variable we can use in development. I also want to create a production environment as well. And let's come in here with a base URL just like that. And for this, we would have whatever our URL is. So let's just
Watch from 19:28imagine this is example.com. You would come in here and all of your example.com stuff would be there. So now we have a development environment and a production environment that both have the same variable. And we can make sure we select our development environment right here. And now we can use that in all of these methods. So, let's replace this entire URL with that variable. To do that, you just open up double curly braces. You can see we have tons options. Specifically, we want that variable we just created. And if you hover over it, you can see what the exact value of that variable is. And when I send along this request, I still get all of my data being returned to me. Let's just make sure we save that. And I want to essentially copy this base URL to all of our different requests. So, let's paste it down there. Give that a save. Paste it down here. Give that a quick save. Same thing for our list. Save that. And then finally, we want our update as well. And we'll give that one a save as well. Now, this next feature I want to show you is one of my favorite features in all of Requestly cuz it makes automating your tests so much easier. For example, one thing that's really annoying is let's say I want to create a brand new author. Let's say I want to give it the name of Kyle 2. I click send. I now have that brand new author. I now need to take this ID. I need to copy that ID. I need to move it into delete, paste it down here. Or if I want to get it, I need to paste that value into here. Same thing if I want to do an update, I have to paste that value here. I actually have to move all over the place using the same value everywhere that I go. This is kind of cumbersome and annoying to work with.
Watch from 21:00This is why it's really nice to be able to create a variables. In our case, we're going to go into this author section. We're going to create a variable called last ID, just like that. We'll give it a save. And this is actually set on our authors collection, which means everything in this folder has access to this last ID variable. And we can automatically set this last ID variable to whatever value we want. For example, when we do a create, I can just get this ID value right here and automatically set it. By using these scripts, we have pre and post request scripts that essentially, before your request goes off or after you get back your response, you can run various different code. In our case, I specifically want to run this line of code. So, we're just wrapping our line of code in try-catch. And we can use RQ, which stands for Requestly. We can access various different things. For our case, we can access the collection variables. These are the variables set on our folder. In our case, our last ID is on our authors folder. Then what I can do is I can set that value by saying {dot} set, pass it in the name of my ID or my variable, which I called last ID. And if I want to get the value, I can use rq.response. That gives me what is being returned from my API. So, that's all of this information over here. I want to convert this to JSON cuz we're getting JSON data, and I want to get the ID property right here. So, now whenever I make a request, it's going to run this code after the response gets back. It's going to take the ID, and it's going to save that ID directly into this variable right here. And then I can use that variable everywhere else that I want.
Watch from 22:18So, let's give that a quick save, and we'll try sending this to make sure that it works. And I gave it a quick send. You saw that we got no errors at all. And if I go into this author section, right now there's no value being set inside of here cuz it's not quite showing up. But the nice thing is if I try to use this value, it should work. For example, I can come in here and I can say that I want to use that last ID variable. You can see the current value is actually set to the value from my latest create request right here. So, it did work properly. And now if I were to do a delete request, it should delete that user. When I click send, you can see no data is returned, and it did delete that user. I can do the exact same thing down here. So, I can say last ID. Just like that. Do the exact same thing for my update as well. Change this to last ID. There we go. So, now any single time that I tried to make a request to create something, for example, I send this along, it's going to take this ID, it's going to save that ID, and now I can, for example, update that user by sending along the request, and it's going to make sure to use the correct ID. Or if I want to get just that user, I can send this along and get it. Or if I want to delete that user, I can delete it, and I don't have to worry about copying around any IDs. Everything's automatically handled for me. We can also make it so that our data is much more automated. For example, right now I'm hard coding this name. I don't want to hard code the name. Instead, I want to use some random data. The nice thing is they have a bunch of random data you use. So, I can, for example, get a random name. Let's just see here if we have a random username, random full name. Let's go ahead and go with that. And now, if I just give that a save and send, you can see every time I send, I'm getting a completely different random name. And
Watch from 23:42since I hooked up all my IDs, for example, if I try to do a get request, it's going to give me whatever the most recent user I created is, or I can delete that user if I want, and so on. In my update function, I can essentially do the exact same thing. I can come in here. I'm just going to put new in front, and then I'm going to get my random full name. There we go, just like that. And now, if I give that a save and I click send, this author is not found cuz we just deleted them. So, let's create a brand new one. And now, let's try to update, and you can see it just puts new as well as a completely random name afterwards. Now, hopefully that little bit of a taste kind of shows you why I really, really love this Requestly tool, and why I was so happy when they reached out to sponsor me. Again, if you want to check out this tool, it'll be linked down in the description below. It's entirely free to use all of this, which is absolutely amazing. Now, the next thing that we need to work on our application is hooking it up to a database cuz right now, everything's hardcoded. It's not great at all. So, let's work on using Drizzle for our database. We're going to be using the brand new version 1.0 release of Drizzle, which is honestly super incredible to work with. Now, in order to install Drizzle, we just need to run npm i Drizzle ORM. And I'm currently recording this before the version 1.0 is released. It's currently in a release candidate. So, if you're watching this video right now before version 1.0 releases, just type in @rc at the end. That's going to get you the release candidate version.
Watch from 25:13But hopefully, by the time you're watching this tutorial, or if you're watching a little bit in the future, 1.0 will currently be the standard release, and you won't need to add that @rc at the end of it. I'm just doing this to keep this tutorial as up-to-date for when Drizzle releases version 1.0 quite soon. We also need to install the development dependency, so just do a dash capital D. And this is going to be Drizzle Kit. This is just making sure we can do migrations and things like that. Again, if you're watching this video right now, you need to use the release candidate version, but in the future it should hopefully be the 1.0 version automatically. Now, if we look inside of our package.json, we should see that we have inside of here DrizzleKit with that release candidate version and the Drizzle ORM. Now, we're going to be using Postgres for our database, so we need to install all of our Postgres stuff by just saying npm i pg. And we can also install the development dependency of @types/pg to get all the types for the Postgres library. And finally, I want to also install .env, so we're going to install .env so that way we can get all of our environment variables working properly in our application as well. Now, to set up our database, we first of all need to create an environment variable, so we'll say .env. This is going to be our environment file, and let's make sure it's included in our .gitignore, which it is, which is great. So, now we want to create a DB section, and inside of this section we're going to have a few different variables. We're going to have our DB password. So, let's make sure we come in here with our DB password.
Watch from 26:30Set that to whatever you want. We'll just say password for now. We're also going to have our DB user. We're going to set that to Postgres. We're going to have our DB name. We can call this whatever we want. Let's just say library YouTube video. We're also going to come in here with our DB host. We're going to set that to localhost cuz we're going to be running this locally. And then finally, our port. The default port for the application is 5432, so we'll just set it to the default port just like that. That hooks up all of our environment variables that we're going to need. Now, we need to run Postgres on our computer, and we're specifically going to be using Docker for that. So, let's create a brand new file. We're going to call this docker- compose.yml. I'm going to paste in what this Dockerfile specifically looks like, but the key thing is we're going to have a services section up here with a database inside of it called DB, and then we need to hook up the image we want to use. In our case, we're using Postgres version 18. Our host name, ports, environment variables, and volumes are all kind of set up based on our environment variables. You can see we're just porting over all of our environment variables, and this volume just tells it Postgres where to set your actual data, and these volumes down here just exposes that data to our computer. Now, don't worry, you don't need to understand how this Dockerfile works or anything inside of it. You can go to the GitHub repository linked in the description and copy this file exactly as is, and as long as you have Docker up and running on your computer with something like Docker Desktop, it'll run perfectly fine once you paste in this configuration file, and then just run Docker compose up. That should start up your application. As you can see, everything was created and is working just fine inside of our application. Now, the next
Watch from 27:48step to work on is to be able to get our Drizzle configuration set up. So, inside of our application, let's create a brand new file called drizzle config .ts. And inside this drizzle config, we're just going to export a default define config. And this define config function should be getting imported from Drizzle. So, let's just make sure we do a quick import define config from and that comes from Drizzle Kit just like that. Now, inside here, we can define a bunch of different things. First of all, the out location is where all of our migration files are going to go. We're going to put them inside of our source folder, and we specifically want them to go in a DB folder called migrations just like that. We then have our schema. This is where our schema is going to be located, so we can say that this is going to be in our source /db/schema. There we go, .ts. Next, we need to specify our dialect. In our case, we're using Postgres, so we can use Postgres as our dialect, and we also want to set strict to true and verbose to true, and that's just going to add extra safety nets inside of this. So, when we do migrations and so on, it'll tell us what code is being run and make sure if we try to run any unsafe code, it'll give us a warning before doing so. Lastly, we just need to hook up all of our database credentials. So, this is going to be things like our password, which we know comes from our environment variables. So, we could say process.env.dbpassword, but doing it like this is not type safe at all. So, instead, I'm going to create an environment variable file for all of our type safe environment variables. So, we're going to create a brand new file.
Watch from 29:18We're going to put this inside of data/env.ts. Inside this file, I'm going to import .env/config. That's going to load up all of these environment variables for us. And then inside of here, what I want to do is I want to create a brand new environment schema using Zod. So, we're going to import Zod. And all I'm doing is I'm taking each one of our environment variables. So, DB password, DB user, DB name, DB port host, DB port. And all I'm doing is I'm making sure that they work with in all of my schemas. So, I'm just making sure that these are values that exist. And I'm also putting a port in here. So, by default, it'll be port 3000. And that's nice because inside of this file, I can for example replace this port with env, which if I make sure I export this There we go. Export Actually, that's our schema. We need to get our data from that. So, let's get our parsed data, which is env schema.safe parse Pass it along our process.env. That's just going to parse all of our data. You can see here. It's going to give us all that information. And we can just say, "Hey, if our parsed data is not successful, then we have an error." So, we'll just say throw new error "Invalid env." And we can just pass it along our parsed.error.message. That way we know if there's something wrong with our environment variables. Otherwise, we're going to export our env air variable, which is just our parsed data. There we go. All this code is doing right here is just adding type safety to our .env file. So, now wherever I have that index file, I can say env. I can import that from that file I just created. And now I have type safety for all my different environment
Watch from 30:45variables, which is exactly what I want. And in my Drizzle config, I can now get those environment variables as well. So, I can say env to get my password. I can come in here with my database. And my database here is going to be env.db name. I can also make sure I get my user, which is my env.db user. Let's see, we have our host env.db.host. We have our port, which is our DB port, just like that. And then we're going to set SSL to false just because we're not working on a secure connection cuz everything is local, so that's why I have that set up as is. Now, in order to use our database, let's go into our source folder, create a new folder called DB, and inside that folder, we're going to create a file called db.ts. We're going to export a constant called DB, which is just going to be equal to calling the drizzle function, just like that. And this drizzle function is going to take in an object, and we're going to be passing it in a connection parameter right here, which comes from our environment variables. So, if we take a look at this, you can see all I'm doing is mapping my password, username, database, host, and port exactly like I did before. And now to get drizzle, let's import that as well. So, drizzle, that comes from our drizzle ORM. There we go, drizzle ORM, and we're using the Postgres version on Node, so we're going to say Node Postgres just like that. So, now we've hooked up our database with all these different connections, and these connections right here. So, now we can properly use our database inside of our application if we want, but we need to add what our schemas for our tables and so on look like. So, let's create a folder called schemas.ts, and this is where our not ts, schemas,
Watch from 32:16there we go. And inside of here, let's create a folder called authors.ts to define what our author table is going to look like. So, to create this table, we can just say we want to export a const, we'll call this our author table, which is using the PG table function, just like that. And we can specify the name for this, which is authors. Then we can specify each individual key that we have. So, for example, we can have an ID, and our ID is going to be a UUID coming from drizzle. We want this to be a primary key, and we want it to be a default random value. So, this is going to be our default ID that we use for everything. We can also say that we have a name, which is just text. Again, make sure this comes from the Postgres version. We want this to be not null, and then finally, we're going to have a birthday. This one is going to be a timestamp. Again, make sure we get the Postgres version. We want to set that to say with time zone true. That's just going to make all of our time zone conversions much easier. And then finally, we're going to have one more field. This one is going to be called created at. And this created at field is going to have be not null. It's going to be required. And it's going to be a default value of now. So that way it's always defaulting to whatever the current value is whenever we create a brand new author. Now we're going to have multiple different tables. I've just broken them out into each of their individual files.
Watch from 33:47But to use them all in one file, we need to create a schemas.ts file. And in this file, we're just going to do a simple export star from and we want to go into that schema folder just like that. So for every table we create, we're going to create an export line right here so we have access to that directly inside of our database. Now what we can do inside of our DB folder is we can pass along our schema just like that by importing our schema. So we're going to say import star as schema from that {dot} {slash} schema TS file just like that. And that's going to import all of our schema related data directly into here. Now technically, this is the old way of doing this inside of it and we need to use relations instead. So we're going to create a brand new file called relations.ts. And this relations file is going to import all of our schema data just like this. And it's going to export a constant called relations which we're going to set to some particular value. And then what we can do in our database is replace this with relations. And up here, we can import relations from that {dot} {slash} relations file just like that. Now to use these relations, all we need to do is we need to use our define relations function and pass it along our schema just like that. And there we go. And as we start to add more tables, we'll define the relationships between those tables. But since we only have one table, we don't need to do anything. So this is the only thing this file has for now. And that has hooked up all of our relations with our database. So, now we can start using our database properly inside of our application. So, now let's
Watch from 35:03go into our route for our author, and let's just completely remove all this hard-coded stuff, and change all of our routes to work properly. So, all of these routes are going to be changed to asynchronous routes cuz we're going to be doing asynchronous data gathering. And the first thing I want to do is I want to get all my authors, which is just call await, and we're going to call db {dot} We want to query. Make sure I import that. Query my author table, and I want to find many, which is just going to find everything in that list, and now we can return that data as is. So, already that's completely solved this section. Let's work on our get section next. So, here we just need to change this code to say await db {dot} query. We want to query our author table. We want to find the first one. And we can use a where clause right here to say where the ID is equal to this ID right here. There we go. Make there sure that is asynchronous, and that solves this entire section. Again, we're just replacing that hard-coded data with our actual database. I can do the same thing down here with our create function. So, instead of doing an authors {dot} push, we're going to say db, and make sure we await this. db {dot} insert. We want to insert data into our author table. And we want to set our specific values just like this. So, to set our values, we can come in here with an array of our {dot} values, or in our case just pass along all of our data as is. We also want to get all of our data, so we can just say returning just like that, and it's going to return all of our data.
Watch from 36:33And if we make sure that this is an asynchronous function, we can then say const author is equal to getting that data from here. And this is actually going to return to us an array of all the data we inserted. We're only inserting one value, so by destructuring it like this, that gives us the first author we just created. So, that's updated our entire create function. Our update function is going to be, luckily, a much easier function for us to work on. I'm actually going to copy the code right here cuz it's going to be very similar. We can just come in here, paste that down. I can do a simple DB update. There we go. And we want to set our data to be this new data. And we need to make sure we restrict where we update, so we can just push a where clause directly onto here, and we can use this equal function to say where our author table.id is equal to our ID. Now, what that code has done, if we make this async, is it's properly done an update function for our author. It's returning all of our new author data based on that update. And this code down here, we can just completely remove cuz that was only there for that placeholder section. So again, just replacing this code with our database code. And finally, for delete, again, this is going to be quite simple because we can just do DB.delete on our author table. And we want to delete where that particular clause is set. So we'll just copy over the exact same where clause cuz we only want to delete in the instance where it matches the ID. There we go. Make sure that's asynchronous.
Watch from 37:51And now we finished setting that up as well. So to make sure that all of this is working properly, let's first go ahead and migrate over our database. So what we can do is we can run NPM run actually NPX drizzle kit. And we want to do a migration, so we can just say migrate. And actually, we should generate our migration first. So we'll say generate just like that. This should successfully work, and it's right now saying no schema files found for this particular path. So let's make sure that our path is correct. I called this schemas. This should probably be called schema to make sure it matches our name. Now, let's just try that command again, and hopefully it should work this time. You can see it properly created our migration, which you can see right here is migrating all of our data. And now, if we just do drizzle kit migrate, that'll actually push all of that data to our database. Now, it looks like we are getting a bit of an error. I think that's because I stopped my Docker, so we can say Docker compose up. That should hopefully set all this back up. There we go. I just stopped it for some reason earlier. And now, if we try to run this migrate, it should hopefully work properly. There we go. Looks like that worked. The reason I was getting an error the second time was because I had a different port being run for a different application, so it was causing an error. But now, I just closed my old Docker that was running and now I have this all working properly. You can see it properly migrated everything and now if we go into our application and do the testing, everything should still work exactly the same as before, but it's persistent in a database instead. So let's just go to our client to test to make sure everything's working like we expect it to. Let's make sure we save anything that wasn't saved before. There we go. And now let's go ahead and we'll just do a simple create. Minimize that down, click send, and you can see it sent all that information and now we're getting that created at field from our database. Our ID is automatically
Watch from 39:12randomly generated and that birthday is set to null because we didn't give it a birthday by default. Let's try a get to see if that works. You can see it's giving us that one single user. If I do a list, it should also return only one user cuz there's only one that's been created. Let's create another one so now we have two users in our database and if we do a list, it's returning both of those users. If I do an update, it should hopefully update our user. Let's give that a quick send and you can see that worked properly as well. So now if I go back into my list, you can see that that user has been updated. And finally if I do a delete, we can also remove that user. If we go to our list, they've been removed from that list. So everything is now being properly synced with a database which is exactly what we want, which means now we can move on to some of the more fun stuff which is dealing with authentication and also dealing with relationships between different tables. So let's go ahead and create a user table where we're going to store all of the information for a user. We'll just copy this author table cuz it's going to be very similar. So we'll say users. This one is going to be our user table. We'll call it users and now we need to specify all the fields associated with a user. And a user is just someone that can log in and view data in our database. So they're going to have an email so we're going to say that that is going to be a text which is not null and also it must be unique so we'll come in here with a uniqueness on that. We also want to pass along a password for the user. So we'll say password, but we don't want to store their password as a plain string. That's very insecure and definitely something you don't want to do. Instead we're going to be hashing that password so we'll call this a password hash. This does not need to be unique. Multiple users can have the same password. So we'll just say it's text that is not null.
Watch from 40:44We're also going to give the users a role. So to create a enum, which is what we're going to use, we're going to come up here with an export const user role enum. We need to set that to PG enum, and we can give it a name. We'll just call this our user role. And then we can specify an array of values. So, in our case, it's going to be either user or admin. That way we can do different things with different permission levels for our users. So, for our role here, we can set it to our user role enum we just created. We can say it's not null, and by default they're going to be a user just like that. So, now we have a email, a password hash, a role. We don't need a birthday, and we have a default created at. That's everything that constitutes what a user looks like in our database. And there is no relation between an author and a user, so we don't need to modify what our relations looks like. We just need to make sure here we export what our schema looks like. So, inside of here, let's export our users just like that. So, now we have both of those available in our database. So, now whenever I type in db.query, I can see I have the author table and the user table that I can query from right now, and that works properly. Now, in order to set up our users, I'm going to take our author routes and completely copy them, so we have our user routes. And inside of here, we want to get our user routes just like that from our users. And we'll just say user routes comes from that user file we just created. So, now essentially {slash} users is going to give me my users routes. Now, I think I'm going to rename this to off actually, so we'll just say off off routes.
Watch from 42:10And we'll call this off as well. And finally here, we'll call that off just like that. So, that just kind of cleans up all that. Let's make sure this import works properly. There we go. And now inside this section, I want to convert everything from this author related stuff to be working with users and authentication. Now, in my application, there is no way to get a list of users or anything like that. So, we're going to completely remove all of our get methods, and instead the only methods we care about is going to be registering a new user or logging in as an existing user. So, we're going to have a post for register, and we're going to have a post for login. All this get and delete and put and update, we don't need those. We just need to be able to create and login as a user, and those are both going to be post request. So, for this register, let's create a schema for that. We'll call this register schema. There we go. And to register a user, all I need is an email and a password. Let's make sure that we set this to an email. Just like that. And this one down here is just going to be a string. There we go. And we don't want this to be optional. Of course, they obviously need a password. And we can specify, for example, it must be at least eight characters long to give us a minimum value. We can do the same thing with a login schema as well. The login schema is going to be essentially identical, but we're going to make sure that the password does not have a minimum requirement cuz if our password requirements change over time, having this be the least restrictive as possible is ideal for what we want. So, now we have a login and register schema.
Watch from 43:36We can use that register schema here. And now what we have access to is all of our data for the email and the password. We just need to make sure we properly hash this password, store it in our database, and then later look that data up properly. Now, I'm going to create a brand new folder where we're going to store all of our hashing and off-related stuff. So, I'm going to say lib just like this. And inside this lib, I'm going to create a folder called crypto.ts for all of our cryptological stuff. So, we're going to have a async function. This one is going to be called hash password. This is going to take in our password as a string, and we're going to convert this to a hashed version. And essentially hashing a password, all as it does is it takes your password that looks like this as plain text, and it converts it to a bunch of random nonsense, but it's deterministic. That means the same password gets converted to the same random nonsense every time, and it's a one-way conversion. The reason you do this is if for some reason someone gains access to your database, instead of seeing everyone's password as strings like this, they instead see this absolute nonsense, and there's no way for them to go from this nonsense value to what your actual password is. It's a one-way encryption, which is why it's called a hash cuz encryption is two-way. You can go encrypt and you can ecrypt, but with hashing you can only go one direction. So, we're essentially creating an irreversible version of our password, so people can't figure out what it is if they get access to our database. So, that's what this entire function is doing. And the nice thing is is there's a lot of built-in functionality to help us do this.
Watch from 44:59Specifically, there's a function called scrypt. So, we're going to import scrypt just like this. This is going to come from the node crypto library. And this gives us access to everything we need to do for hashing our passwords. So, first we want to create what's called a salt. So, we're going to say salt. This is going to be a completely random bit of data. So, we'll call random bytes, pass it along 16 here to give us some random data. And we'll convert this to a string. And this is going to be a hex-based string. Now, the reason we use salt with our password is because, like I said, whenever you convert a convert a password, it's a one-way conversion. So, password always converts to the same thing. And if I pass in the same exact string, it's always going to convert to the same string on the other end. By adding in a salt, it's just random data that gets added in at the end of our password. That way, the output data is also somewhat randomized. This is useful because, if for example, I know that converting password always gives me the value 1 2 3 4 5, if I am a hacker and I get access to your encrypted or your hashed passwords, and I see the value 1 2 3 4 5, I know that that actually means the user's password is password. I can essentially do a reverse lookup because I know what the one-way conversion is. By adding in this random bit of data at the end, it now takes this thing that the hacker knows the value of, and it changes it to something else that's entirely different. They don't know what the value of this is. So, this is just to help you against what's called table attack or a dictionary attack or rainbow attack. All these different terms are essentially the same thing. We're just helping make this even more secure than it was before. So, now with this salt and this password, we can create the
Watch from 46:21hashed version. We're going to call await, and we're going to call scrypt async just like that, pass it in our password and pass it in our salt. And we're just going to create a nice little a sync version because this s crypt version uses callbacks, which are not really ideal. We want to use promises. So, we're going to create an a sync function called s crypt a sync, which takes in our password as a string and a salt that's a string. And essentially, we're just going to return a new promise that is going to wrap what we had before. So, this is going to be our request and our response. And actually, it's supposed to be our resolve and our reject, just like that. There we go. And what we can do is we can call that s crypt function. This s crypt function takes in a password. It takes in a salt. We're going to pass along the number 64 here. That's how long our key is going to be. Essentially, that just is how long we want our password and encryptions to go for. And then finally, we get a callback. This callback is going to give us an error and it's also going to give us our derived key, just like that. Now, if we have an error, we can just reject that error. So, we'll say reject and pass along the error, just like that. Otherwise, if we don't have an error, we can resolve the proper thing. So, we'll just do a quick return right there. Otherwise, we'll call resolve with our derived key. Now, our derived key, if we look at the type here, is this non-shared buffer. We can actually generic this type slightly by saying that it is going to be a buffer.
Watch from 47:51And this buffer is an array buffer, just like that. That's the generic type that we want to use specifically for this section. Now, I shorthanded this to just already have it. So, we'll shorthand this right there. And now, we have this s crypt function completely written. We have our hash value right here. And essentially, that's all we need to do. So, now what I can do is I can return that hash, but I want to make sure the salt is also part of that hash. So, I'm going to return my data as salt and I'm going to put in my hash as well. So, here's my salt at the very start. And then I'm going to put a colon. And afterwards, that is going to be my hashed password. And I need to make sure I convert that to a hexadecimal just like that. Close this off and now if I give that a save, essentially what I've done is my password is now storing the salt at the beginning of the password and it's storing the hashed version of that at the very end. So that way when I get my data from my database, I can just split on this colon to get my salt and to get my password data. So let's go ahead and go back into this off section here. The first thing we've done right here is we've tried to insert a brand new user into our database. Clearly, that's not what we want to do. The first thing we want to do is we want to say, is there an existing user in our database? So we'll say existing is equal to await db. We want to get a query for the user table. We want to find the very first one and we want to find them where our email is equal to the email that comes in from our data. So we can come up here and say that the data we want to get from here is our email and our password.
Watch from 49:16So this is going to figure out, do we have an existing user or not? If existing is equal to null, well that means that we don't have an existing user, but if it's not equal to null, that means we're trying to register a user that already exists. That's obviously not something we want to allow, so we're going to throw an error. We'll just say, email already in use. And there we go. We can send along a 409 status code for this one as well. A 409 status essentially means that the user's data being passed up is conflicting with certain things that we expect and hopefully they can fix that data themselves. It's very similar to a standard 400 error you may have seen exist, what we can do is we can get the hashed version of our password. So we'll get our password hash by calling that hash password function and passing it in our password just like that. This is an async function, so we'll make sure we await it and then we can get our user by just calling that same exact db.insert into our user table. We want to insert specifically values and the values we want to insert are email and our password hash just like that. That's all the information we want cuz again, this password hash, if we remember, it includes our salt and our password, which is what we need to essentially reverse engineer what our password is. We also want to make sure we return all that user information, but we only care about specific fields. For example, I care about the ID field and the email field, so we can say user table.email. I don't want to pass down the password or anything like that. That would be very insecure. Finally, I can pass my user down just like that. Now,
Watch from 50:42if I just make sure I await this because this is an asynchronous function, that should be everything I need to do to register a user inside of my database. So, let's go ahead and test this out before we do our login section next. So, to do this, we can create a brand new collection. We're going to call this collection off and inside that collection, let's go ahead and we'll create a brand new request and this request is going to be for register. We know that this is going to be a post request and it's going to use that base URL. And we want to make sure that we go to off/register. Actually, we don't need any /register. It should just be /off, I believe. Actually, I believe it is register. I lied. So, let's just give that a quick save. Now, let's go ahead and set up our body to pass along our JSON to register a user. We know to register a user, we need to pass along an email and of course, we can use that random email information. So, we'll say random email and we also need a password. This doesn't really matter what it is, so we're just going to set it to password just to hard code a value just like that. So, now, if I give this a save and I click send, we should hopefully be creating a user, but it looks like we're getting some type of error. So, let's go ahead and actually take a look at what that error looks like and it looks like here, it's failing cuz our database table doesn't exist. That's cuz we need to re-push our database. So, let's create a new migration and then we're going to migrate all of that data into our database and that should hopefully create that brand new table for us. Once that's done, we should hopefully now be able to send this request along and actually get a user created. And if we look over here, that does look like it worked. We have a user with a brand new email and we have an ID for that user.
Watch from 52:14So, now we have have user created in our database, we can go ahead and we can log that user in, which is the very next step of our application. So, what I'm going to do is I'm going to come in here. I'm going to copy this entire post cuz our login is going to be relatively similar. We're going to call this login. We're going to use our login schema, just like that. And of course, what we want to do is we want to get what the user we're trying to login is. So, we'll say user, just like that. We're going to try to find the first one where their email is equal. And if for some reason our user does not exist, so if our user is equal to null, we'll tell them that, "Hey, for example, this is an invalid email or password." Invalid email or password. There we go. And we're going to send along a 401 status code, which essentially means that you failed to authenticate properly. Now, all the code down here we can essentially remove. And instead of hashing our password, I want to verify it. So, I want to check, is my password valid? And we can use a function called verify password, which is going to take in the password they type in, as well as our hashed password from our user. And we want to check to make sure that these are the same exact password. So, let's go into our crypto function, and we're going to create a brand new asynchronous function called verify password. We know this is going to take in a password, which is a string, and it's going to be taking in a stored hash password, which is a string as well. And all we want to do is we want to compare these two values. So, first we need to get our salt and our hash based in hexadecimal cuz that's how we saved it. You can remember here, our salt came before the colon, our hexadecimal hash came afterwards. So, to do that, we can take
Watch from 53:39our stored hash and we can split it on the colon symbol. That's going to give us these two halves together. Next, what we can do is we can get our stored value by saying buffer.from, and we can take our hash that is in hex, and we convert it away from hex. So, essentially what this little bit of code is doing is it's taking this hexadecimal string and converting it to a buffer array, which is what we need to be able to compare these two passwords properly. And that's because we're going going using a function that is called timing safe equal. And this timing safe equal function is going to take in our stored value. This is the one that comes from our database. And then it's also going to take our derived value and compare them together in a way that you cannot use timing attacks against. Essentially, timing attacks are kind of fancy attack that depending on how long an equality takes, you can determine how close your password is to the correct one by using this function that gets rid of that entire vulnerability. So now all we need to do is get our derived password. So we can say const derived is just equal to calling scrypt async and passing along our password as well as that salt. So what I'm doing is I'm using the exact same salt from the stored database password plus the password my user typed into the form and I'm encrypting that using scrypt. So I'm hashing that and I'm comparing it to the already existing hash and I'm saying if those two values are the same, that means they typed in the same password both times. If they're different, that means they typed in different passwords.
Watch from 55:09That's all this function does right here. So now inside here, we can import that function and if this returns true, the passwords were correct. But if it returns false, so if it is not valid, then we know that they have an invalid email or password. So we can send along that information. And the reason I'm specifically telling them invalid email or password for both is because if I said up here invalid email instead of invalid email or password, they would know they have an email of a user in our account system. So that's generally why it's best to always say invalid email or password for these types of things. Now in order to say what user we are for our different validations in our application, we're going to be using JSON web tokens. And luckily, Hono makes it really easy for us to do that. So we can use a function called sign. This is coming from Hono. It's an asynchronous function. And this function just allows us to sign a JSON web token. Make sure we import that from Hono/jwt. And with this sign function, we can pass a lot along a lot of information. We're going to pass along an expiration data for how long we want this to work. First of all, we need to pass in what our current time is, so we can say math.floor of date.now / 1,000. That's going to convert the current time to the correct format that is expected for this expires time, and then we can just set a default expiration. So, we can come up here and say const JWT expiration in seconds.
Watch from 56:32And we can specify how long we want this to be. So, if we say like 5 * 60, that would be 5 minutes. And that would be how long our JSON Web Token last. So, essentially by coming here and saying now and we add along what that expiration time is, we're saying that this expires 5 minutes in the future. So, our JSON Web Token last for 5 particular minutes. Now, the next thing we can do is pass along any data we want. For example, we can pass along the subject that is our user.id. And we can also pass along other information like the email. So, we'll say user.email. And now we have access to the user's ID and the email whenever we try to access this JSON Web Token. And this is going to give us a token as a particular value, and we can send that token down to the user just like this. And we don't even need a status code associated with that. Now, the final thing we need to pass along to this is going to be some type of secret value. So, we're going to get this from our environment variable. And we're going to call this JWT secret just like that. So, let's go into our environment variables, and we're going to create that JSON Web Token secret. This is going to be a string with a minimum value of one. And now we need to make sure in our environment variable for our JWT section we create that JWT secret. Now, there's lots of ways you can create this secret.
Watch from 57:51You could just hardcode some random value, but I like to do to create this secret is just open up a random terminal for Node, and I like to type in buffer.from. That allows me to create a buffer. We're going to use crypto.random I'm sorry, get random values. And I'm passing in a new Uint 8 array. And then I can specify how long I want that array to be, so we'll say 16 values. So, I've essentially created an array that is 16 values long. I'm converting that to a buffer, and then I'm taking that buffer and converting it to a hexadecimal base to string. If I make sure that I close off all my parentheses properly, and I make sure that I spell uint array properly, cuz this I should be lowercase. There we go. You can see that's giving me back a value. And the reason this value doesn't look perfect is cuz I messed up my parenthesis replacements. There we go. So, now if I put that value there, you can see it just gives me a random string of data. And this random string of data is nice and secure, and I can use this string of data, and every time I generate this, you can see it's just another random piece of data. You want this to be as random as possible. That's why this is what I like to do to create this random data. Now, I have that JSON web token secret. That's what signs my JSON web token. So, essentially the way that this function works inside of our off is we create a token, we sign it with this secret value that only we have, and then when the user wants to do something, they send that token back to us, and we're able to use this secret value to decrypt this code and get access to what the user's ID and email information is. So, let's test to see if this works. Let's come over here with our Requestly. Let's just duplicate this one for register. We're going to call this login, just like that. Our body is
Watch from 59:07going to use a email that we're going to paste in whatever actual value is. We're going to have a password here, and when we send this along, we should hopefully see Right now, we're not getting any data. There may have been something weird going on though, so let me just make sure I resend that. Still not quite working. Let's make sure that we actually give this a proper name of login, just like that. Give that a quick save. And also, let's make sure that we pass it along the correct information. So, inside of our register, let's just send along a register. And it looks like that's not working as well. I wonder if our server may be stopped working at some point, so let's just go ahead and check our server. Looks like there was some errors. Let's just try rerunning that and see if that works properly. Looks like there's no more errors. That's probably cuz of our environment variable stuff. So, now let's give that a quick send, and And can see that did work properly. We can have an email for our user. We can copy that email, go into our login, and where we have our body for our email, we can paste that email in, click send on this, and you can see we're generated a brand new token, and that token can then be used to send along with other requests and information associated with it. Now, again, to make this process a little bit more automated, I'm going to go into my auth variables. I'm going to create a variable called last email, just like that. We'll give that a quick save. Then, what I want to do, just like I did for my post response request here, inside of my register in a script, I want to make sure that I set my last email equal to my response.json.
Watch from 1:00:39email, just like that. And this should be in the post response. So, let's make sure we're in the post response section. There we go, just like that. And now, if I give that a quick save, and I just click send, that should save that email inside that last email variable. And now, when I do my login, I can just use that last email variable, just like this, instead. And now, when I send, you can see I'm getting a token for whatever the last email I used for my user was. Now that we have all this set up, we have a token that we can use anywhere to authenticate as this user without passing along an email and a password. And JSON web tokens are great for short-lived authorization, but they're not great for long-term authorization like APIs, because you if you have an API key, you want to be able to revoke that API key, and JSON web tokens are not something you can do that with. So, what we want to do is we want to be able to convert this JSON web token into a long-term API key that we can use wherever and whenever we want. So, the next step is to be able to create API keys that read this JSON web token to make sure the user's authenticated, and then we can do whatever we want from there with that API key. So, let's go ahead and work on API key sections next. So, inside of our database, we need to create a brand new schema, and this one is going to be for API keys. Now, you've seen me type these tables out a bunch, so I'm going to copy over what this table is and explain it in depth. You can see we create a table called API key tables called API keys.
Watch from 1:01:59We specify an ID, we specify a name, a hash for the key, again a one-way hash, that way if someone accesses our database, they have no way of getting what the API key is. And we also are passing along a key prefix. So, this key prefix essentially is just storing the first eight values of our key. So, if our key is just a key that looks like this, super super long, whatever it is, and it just goes on and on and on and on and on, our key prefix is just going to take the first eight values of those keys and store it in our database. That way when a user looks at their API keys, they know, "Oh, here's the first eight values. I know what API key that is." That way they can use it without actually storing the full API key in our database, cuz we don't want to store the full thing. We don't want users to be able to access it. We're going to use the exact same hashing method we used with passwords to make this more secure. Also, API keys are going to be associated with users. So, here I'm setting up a references that allows me to create a foreign key between my user table and my API key table. So, this is just pointing to my user table. And if I delete my user, it's going to delete all the API keys associated with them. So, now inside my schemas, let's make sure that we take our API keys and we export them. And now we have two tables that have a relationship, our API key and our users. So, in our relations, we can actually set up what our relations are going to look like. This is going to take in a function which just returns an object, and you can see all three of our tables. So, our API key table, for example, this is going to have a relationship of a single user, so we can call that a user. This name right here is whatever you want it to be. And this user value right here, we need to make sure we call r.1, cuz this is a one
Watch from 1:03:18relationship, a API key has one single user. And we want this to be on the user table, so they have one user. And then we have a from field, just like this. So, this is saying what key table are we coming from? Well, we're coming from the API tables user ID, and we're going to the user table ID. So, this is just linking these two table columns together, just like that. And I can essentially do the exact same thing for my user table, just like this. They have many API keys, so we can say API keys, just like this. This is a many relationship on that API key table. And when you have many relationships, you don't need to store any additional information. So, you can see I now have that many relationship set up. So, now we need to create a route for this. So, I'm just going to take my off route, call this API keys or API key, just like that. And our index, we'll create one called API keys. This is our API key routes. And if we just copy this down, just like that. There we go. Now we've hooked up that route at {slash} API keys. And generally with routes, they're going to be this kebab case. So, we're going to kebab case this just like that. And now we can work on this API keys route. And this one is going to essentially have a way to list all of our API keys, as well as to create and delete API keys for security purposes. So, let's just go ahead and actually clean this up drastically. I'm just going to delete pretty much all the code we have inside here, so we have a fresh fresh slate to start with. I'm going to do a simple get method to be able to get an API key. This is going to take in my context, which will be asynchronous cuz we're going to be doing some database request. And what I want to do inside of
Watch from 1:04:48here is I want to get all my API keys for my user. But how do I know what user I'm currently logged in as? This is where we access that JSON web token, and we're going to be using middleware. So, to use middleware for all of our different routes, normally you've seen us pass along middleware here, like our S validate function. We passed that along here as a middleware for individual routes. We want to add middleware to all of the routes inside this section, we can say app.use, and we can pass along our middleware. Our middleware is called JWT, again coming from that Hono JWT. And inside this JWT right here, we can pass along our options. So, for example, our secret is our And we also need to pass along the algorithm that we're going to be using. Now, our algorithm is going to be HS256, just like that. And in order to make sure this is consistent across everything where we sign our JSON web token. So, in this off section, I also want to pass along the algorithm. So, we can pass that along as the third parameter. That way we know we have the same algorithm being used when we sign our keys as well as when we decrypt what our keys does. And this middleware just automatically decrypts what that particular value for our JSON web token is going to be. And we can have access to that JSON web token value. Now, in order to make sure we get all of our type safety set up properly, we're going to create a type here and this one's going to be called JWT ENV. That's for our environment. And this is what we pass along to Hono. We can pass along an environment and this environment contains things like variables. So, we can say that we have a custom variable and this variable is going to be called JWT payload and we can give it whatever type we want. In our particular case, if
Watch from 1:06:18I make sure I spell this properly, our JWT payload contains that sub that is our user ID. It contains our email, which is a string, and it contains the expiration, which is going to be a number. This just matches up with all the stuff we passed into here. You can see this is a one-to-one match. So, we're just making sure we match that up. And if we pass that along as our type to our Hono JWT, now we have access to that in this variable section. So, we can see c.var, that's how we access our variables, and you can now see we have our JSON web token payload, which contains all three of those different properties. So, if I wanted, I could, for example, destructure out just the sub portion and rename that to user ID cuz we know that is our user ID. And now what I can do is I can get all the keys for my user by just saying db.query. I want to query that API table. I want to find all of them where my user ID is equal to that user ID. And I specifically only want to return certain columns. So, we'll come in here with columns and say the only columns I want to return is going to be the ID column, the name column, the key prefix column, and the created at column. Essentially, I don't want to return the hashed version. So, I don't want to return that key hash because that would be insecure to be able to send that to the user and not necessarily needed. Then I can return my JSON for all my keys, just like that. And now that is hooked up the ability to get my API keys from our database. Now currently, we don't have any API keys stored in our database. Let's make sure that this JSON web token stuff is working like we expect it to by hooking this up with Requestly. So what we need to do is we
Watch from 1:07:49need to create a brand new collection. We'll come in here. We're going to call this one API keys. Give that a quick save. And inside of here, let's make sure that we have a request. This request is going to be our list request. So we'll call that list. And we know that it's going to take in our URL for our base URL, API keys, just like that cuz we're just trying to list out those API keys. Now if we just try to call this as is, we should get some type of error or warning. If we send this along, you can see automatically we get unauthorized sent to us because we don't have any JSON web tokens being sent along. We need to send those along inside of our headers. Now I could specify my headers right here for my authorization and specifically this authorization section right here. But instead, I want to inherit this from the parent cuz every API request is going to use the same off. So here, I'm going to specify my authorization and we're going to be using a bearer token. That's what's expected using JWTs. And here, I'm going to use a variable for this. We're going to set that variable to JSON web token, JWT. And we can create that right here. So let's create a variable called JWT, just like that. So now I have this JWT variable. If I give that a quick save, that is being used and I can set that variable or do whatever else I want with it. So for example, when I try to log in as a user, and let's make sure that I register as a user first, and then we'll log in as a user. You can see I'm going to get this entire token. I'm going to copy this over. I'm going to go into my API section. I'm going to paste that down as my current value. Give that a save. And now when I try to list this out, everything should work. I click send.
Watch from 1:09:19And right now we're getting an internal server error. And I already know the reason for that. It's because we never migrated our database. So let's go ahead and make sure we do that. So we'll come in here. We'll generate all of our migration data. And then we'll make sure that we run a migration. That should send everything over. And now hopefully when I do a quick send here and I look at this, you can see I get an empty array being returned. So it is properly getting our data, sending it along, and getting that information from our headers because of our API key right here. Now to make this a little bit more automated than what we're currently doing, I'm going to do a same thing with my post responses here. I'm going to do this with our login. So for our login post response, I want to store an environment variable. So we'll say environment. This is going to be my JSON web token. I want to get specifically the token response from here. So I'm going to be creating a variable called JWT in my environment that's going to be set to whatever this token is. So now what I can do, instead of storing my variable here, we're just going to completely delete this. We don't need it anymore. What I'm going to do is I'm going to create an environment variable here called JWT. There we go. Give that a quick save. And now that's where my variable is going to be stored, and everything is going to be automated for me. So if we look inside of here, authorization, you can see that's being pulled from the environment properly. And if I were to log in as a user, it's going to save that token inside that environment variable. So now if I try to send this along, I still get that empty array. If I hover over this, you can see it gives me that current value, which is that value that's coming from that environment variable. Again, we're using Requestly to automate all of this process for us. Now let me show you how we would actually go ahead and create our own API keys cuz right now we're getting API keys, but we have no way to create them.
Watch from 1:10:46So let's do an app.post. We want to just post this main route. We want to do an SValidator here. This is going to be for JSON. And we specifically want to come in here with our create key schema. That's going to be a schema that we create. And then we're going to get our asynchronous function right afterwards. So let's create that right now. We can say const create key schema. That's a Z object. Make sure I import Zod. And the only thing this needs to have is a name. So we'll come in here with a string. And we'll give it a minimum of one. Let's say it has a max of 255. So, we're able to name what each of our API keys is. That way, when we get that data, we know what the name associated with the API keys is, as well. So, now we can do inside of our function is we can use the exact same code up here to get our user ID, so we know what user we're trying to create for. We can also get our body. Specifically, I care about the name portion. So, we can say await c.request and we want to get the valid portion of our request for JSON, just like that. That's going to give us this name property that we want to use. And then I want to generate an API key. So, we'll say API key is equal to generate API key. And this is a function that we're going to create, which will just generate a generic API key. We'll do it in this function right here for our crypto. So, export async function generate API key. Now, this is going to be a very basic function. First of all, we're going to get a raw key. That's just going to be some random bytes.
Watch from 1:12:17We'll say it's going to be 32 random bytes. And we're going to convert that to a base 64 string. So, all we're doing is we're just getting a bunch of random data that's going to be returned to the user. Then we're going to hash that data. So, we can say hash API key. And actually, instead of creating a custom function for this, we're just going to write this inline. We're going to call create hash coming from node crypto. We're going to pass it in our hashing function. We're going to use SHA-256 for this. We're then going to call update on this by passing it in the raw. And then we finally say a digest of hex. Essentially, what that's doing is creating a hashing function using the algorithm of SHA-256. We're then going to be hashing this raw value. That's what update raw does. And then we're converting that to a hexadecimal value. The reason I'm not using this fancy scrypt function is cuz we're not doing any salting or anything like that. We're just hashing this, so it's slightly more secure when we go ahead and try to use it. Next, we want to get the prefix from this, which is just going to be our raw value.slice. And we're going to slice off the first eight things from it. So, essentially, this gives me the first eight values of my raw strength. Finally, I just want to return all this data as is. So, that way I can store this in my database. So, now if we go over into our API keys, we have generate API key. And this gives me inside of here if I make sure I await it. There we go. It gives me my hacks, my prefix, and it gives me my raw data. And this function actually doesn't even need to be asynchronous, so we can completely remove all the async code from here cuz nothing inside of here is asynchronous.
Watch from 1:13:48This hashing is entirely synchronously done. Now, back to that API key section. We have essentially everything we need to do to insert this into our database. So, we can say db.insert. We want to insert in our API key table. We want to set some values. We want to set our name. We want to set the user ID. We want to set the key hash, which is just set to our hash. We want to set our key prefix, which is our prefix, just like that. And that creates a brand new API key for us. So, we'll say const API key is equal to awaiting that. And since this returns an array, let's make sure we do that. And the only value I care about returning from here is going to be the ID. So, we can just come in here and say ID is going to be API key table. id. Cuz I just want to get the ID of that API key so we can use it in other places. Now, I can return down my JSON. So, we'll say c.json. I want to return what my raw key is as well as the ID for that API key. There we go. And we'll set along a status 201 saying we created this. The reason that I'm setting everything up like this is because now when they create a key for the very first time when they create it, they can see what the full API key is. And now every other time they try to access their API key, they can never see the full version. This is a security measure. They only see it one time. They can use it in their application, and if they ever maybe lose that API key or forget what it is or it gets hacked in some way, they can come here, delete it, and create a brand new API key and replace that inside their application.
Watch from 1:15:12This is the most secure way to handle API keys pretty much anywhere that you're going to work with them. So, now let's set this up inside of our quest list so we can work on creating API keys as well. So, let's just come in here. I might as well just duplicate this one. This one right here is going to be called create. It's going to be a post method just like that. It's going to be going to that API keys endpoint. It has the exact same authorization inherited from the parent like we did with the other one. The only difference is we need to pass along some JSON in the body. So, let's come in here with some JSON and this is going to be the name field. And inside of here, let's just do like a random company name or something like that. Sure. There we go. It doesn't really matter. It's just going to be the name of our API key. So, now if we give that a quick save and I click send on this, you can see currently we're getting unauthorized being sent along. Most likely because our JSON web token has expired. So, at least our expiration is working properly. Let's try logging in properly. That gave us a new one and now if we send along create, you can see it's giving us a key. There's the full value as well as an ID for that. And if we try to access our list of these values and I send this along, you'll notice here I'm getting that name and I'm only getting the first eight values from my key. It never returns to me my key again. The only time I can access it is the very first time I create it. You can see there's the full key. Otherwise, every other time it just gives me a partial version of the key. Now, just like we did with all of our other things, I want to set up a post response script for this. So, let's just go to register cuz that is essentially what we want to copy. So, I'm going to copy that over into this section right here. I want to set a collection variable for whatever my last ID is and set it equal to that ID. And I also want to do the exact same thing with environment
Watch from 1:16:36variable. That's what I did for my login section. I created an environment variable. So, we're going to create an environment variable here that's going to store what our key is. So, we're going to call this API key and we're going to set that to whatever our most recent key is. So, now I have access to both my most recent ID and my most recent API key. And if I go into my environment for dev, I can make sure I set API key right here as a value. There we go. So, now I can actually use that anywhere else I want. So, now when I create an API key, send this along, it should be storing all that information. Now, right now it says it could not find the collection variable. Let's make sure we set that. Last ID, give that a quick save. There we go. Looks like that worked. Inside of this section, let's make sure we save that as well. And now, if we send this along, we should see that everything is working properly and that value is being saved as our current value. That way, when we set up our delete, which is what we're going to do next, we can make sure we hook it up the same way we did delete for our authors and so on with this nice little last ID trick. So, now we have our API keys. We can list all the API keys for our user. You can see they have a bunch of them right now. Let's go ahead and work on being able to delete those API keys as well. And this is going to be relatively simple. We can come in here with a delete for a specific ID. We can make sure here we get our ID, which is just request. Whoops. C .request .param and we want to get the ID specifically, just like that. There we go. And now, we can just do a simple delete. So, we can say delete from the table and we can just do it a where clause.
Watch from 1:18:07And we want to do it where equals API key table.id equal to that ID that we pass in. We also want to make sure that the user ID is equal as well. So, we'll throw an and into here. And we'll set an equal on there that the API key table.user ID is equal to our user ID. There we go. Make sure I get my imports all done. Don't need to return anything. And that right there is going to be to our delete entirely done. We can just return a null body with a 204 saying that we deleted this. So, let's go ahead and actually test this out to make sure that it works. We'll come in here. We'll create a brand new API key. We'll just give that a quick send. Now, we have that key right here. That last ID is already being stored. We can create a brand new request. I might as well just duplicate this one right here. This one is just going to be called delete. We'll send that delete status along. And again, inside of our parameters here, we want to delete a specific ID. And we can just set that value to our last ID, just like that. And everything else should be working automatically for us. So, let's just try to send that along, and you can see nothing happened, which is actually good. There's no response. Give that a quick save. So, now let's just do a double-check to make sure everything works. Let's create an API key. We're going to click send. You can see this one starts with SRSIV8.
Watch from 1:19:32Whatever, it doesn't matter. When we go into our list, and we look at the very bottom or somewhere, we should see that one. Yeah, there we go. SRSIV8. That's the one right there. If we click delete, it should delete that most recent one. So, if we click send, you can see that that one is no longer in our list. So, our delete is working like we expected to. And now we have access to an API key, which we can use to restrict certain things. Like, I don't want to let anyone create an author. I only want to let logged-in users create authors. That way I can make sure to associate them with it, and so on. So, to do this, I actually want to set up authorization that works on our entire section here. And what we can do is we can set this to API key. We can set whatever key we want. I'm going to just going to use the default of X-API-Key. And then we can use a value here. I'm going to use that variable we created called API key. And this is going to be added to the header of every single request. So, if we give that a quick save, now every single request that gets sent along that inherits from this is going to require an API key. For example, our off section, we don't want to inherit from this. So, we're just going to say that it has no off. So, we're going to clear that out. But our author section, you can see in authorization, it inherits from the parent, which means it's going to automatically send along that API key. So, every request we send to our author section, for example, create, is going to send along that API key. Now, right now we don't have anything being sent along. Let me just try to register as a user. Let me try to log in as a user. And when I do that, I'm still getting an error. I think that's because this authorization is also inheriting from the parent. Let me just clear that out. That way there's no authorization for our login and register. And now when I send, you can see it is properly trying to send that information. So, when we try to log in, it's going to log me in properly, gives me that token, and now I can use that
Watch from 1:20:51token to create an API key, which now that API key is being set inside of everything. So, now if I were to go ahead and try to like access my author, I'm able to access it cuz an API key is properly being sent along. So, now let's go modify our code so we can access that API key. Inside this section that we have for our auth, so we have our auth right here. This is where all of our authentication code is going. I want to create a brand new section for our middleware. So, up here inside of our source, let's actually just create a brand new folder called middleware. And in this middleware folder, I'm going to create one called auth .ts. And this is where we're going to create a function API key auth. And I'm actually going to create that as a constant variable and set that equal to the create middleware function. There we go. That comes from the Hono factory. And this create middleware is going to take in a generic type. So, we're going to create a type for that. That's our API key ENV. Kind of the same exact thing we did for our JSON web tokens, but this is for our API key. So, that's going to take that in here. We'll set that up in just a second. And then this is going to be an asynchronous function that takes in our current context, as well as a function called next. And this next function just allows us to say, should we proceed or should we immediately exit out? Now, for our API key, we want to set variables just like we did with JSON web tokens. And we specifically want to set one called our API key user. And this is just going to be equal to our user type. But, we're going to specifically pick only certain fields from it. So, we're going to say type of user table.infer
Watch from 1:22:22select. And I want to pick just a few fields. I want to just get the ID. I want to get the role. And I want to get the email. So, those are the only fields that I want to get for my user. So, we're going to be creating a new variable and sending it on our context called API key user. Now, to do that, we first need to get the API key itself. So, we can say const key is equal to c.request .and we can come in here with our headers. And we created that header. We gave it the name of X-API-Key. And me make sure I spell that properly. There we go. And now we can just say, "Hey, if we don't have a key, obviously you have a problem." So, if our key is equal to null or our key.trim is equal to an empty string, that means we don't have a key. So, we just want to return some type of error saying that they aren't authorized to access this. So, we'll say error missing API key. Just like that. We'll send along a 401 saying, "Hey, you didn't authenticate properly. You're supposed to have an API key to access this part of our API." Next, we can get the key hash, which is just equal to hash API key. And this is actually a function that we should create. So, inside of our crypto section, where I hardcoded that function, let's just call this hash API key, which takes in our key. And then let's export that as an async function hash API key.
Watch from 1:23:53Takes in a key. And then I'm going to just run that code by taking in whatever our key value is and returning that to us. That way we can use that in other places, such as here. So, now we can hash that API key by taking the key from that, and we can now compare these hashed keys just like we did when we did the password comparison. So, let's get our API key just like that. We'll say await DB query. I want to query that API key table. I want to get it where my API key, I want the hashed version specifically. Let me just make sure I have this proper. Actually, this is the API Oops. db.query .api key table.find first where and we want to get that key hash is equal to this key hash right here. Now, I accidentally set this as an async function, so let me remove that async from there. That gets rid of our errors that we have. And now we can just check, "Hey, if we have an API key, that is at least one step further." So, if our API key is equal to null then we want to throw a type of error again saying invalid API key cuz they sent along an invalid API key that doesn't exist in our database. And finally, once we have an API key, we can figure out what user that API key is associated with. So, we can say await db.query. We want to query the user table. We want to find the first one where the ID of the user is equal to our API key .user ID, just like that.
Watch from 1:25:13And I only want to get a few columns from this. And these columns are just going to be the columns up here, ID, role, and email. So, I'll say ID, role, and email, just like that. So, now we have our user object, and we can now use that. But, of course, first we want to check to make sure the user exists. So, if our user doesn't exist, again, we'll say they have an invalid API key, just like that. And then finally, at the very bottom, once we know they have a valid API key and it's associated with a user, we can say c.set. This allows us to set a variable, for example, our AP key API key user. We can set it to our user like that. And then we can just call next, make sure that we await it properly, and that's going to make sure that it calls the next thing in line, which is going to be the function that uses all of our API key stuff. So, this right here is just a middleware. And the nice thing about middleware is we can use this anywhere we want. So, inside of this author section, we have our routes here that anyone can access, but I want to make sure that creating, updating, and deleting are going to be protected routes that use middleware. Well, what I could do is I could paste the middleware into each one of these like this, and I could make sure it works like that. But, this is kind of cumbersome. So, instead, what I want to do is I want to create essentially a set of protected routes that all share that one piece of middleware. This will also make sure I get full type safety as well. So, what I can do inside of here is I can create a protected app, which is a new Hono app. Make sure I give it that API key environment type.
Watch from 1:26:37Looks like I probably didn't export that, so let me make sure I do that. So, inside that middleware, let's export this type. Then we can use it everywhere we want. So, here, make sure we import that. And now, with our protected app, I can just call that API key by saying dot use just like this and now it's using that API key off middleware that we have and it's going to use it everywhere we use protected app. So here for the post, here for the put and here for the delete, we're going to make sure all of these are protected and are going to have access to that API key user if we want it. So for example inside of each one of these, I could say I could get the user that is currently being authenticated inside this section. Now I don't need that user information but I already have the validation set up in the middleware so let's test to make sure that this works like we expect it to. Right now, I should be able to access all of these fields and if I look, you can see I'm able to get, I'm able to list, I'm able to do a create. Right now we're getting a 404 not found which that doesn't make any sense. This is most likely because somewhere inside of our database queries is having a problem. So inside of our middleware, somewhere in here we're throwing some type of 404 is my guess. Let's just look at our console logs to see if anything inside of here is telling me anything. It is not. Let's just put in a a quick console log. We'll say console.log key to see if the API key is being passed along. So now what I can do is I can click my send, still showing me that 404. If I look in here, it's not logging out anything at all. Okay, I know what the issue is. We created this protected app but we're not actually exporting it.
Watch from 1:27:59We're only exporting our normal app. So what we need to do is we can say app.route at the base route, we can add in our protected routes. There we go. So that's just going to add in all of our protected app routes to our normal app so that way they're all available everywhere. So now we should be able to access this wherever we want. So let's bring over that request and let's just quickly send this along and it does look like now it's properly sending along the information to create and we should be able to do a delete as well and we can do an update which will fail cuz the author is not there cuz we just deleted them. So let's try another update just like that. Now what I'm going to do is I'm going to change my API key to make it incorrect. So I'm going to go to my dev environment where my API key is. I'm just going to type a bunch of random data in there. Now it's an incorrect API key. Now if I go ahead and I try to create a user, I'm going to get an invalid API key. But if I try to get a user or a list of user, this is going to work fine. You can see list works fine and get also should work fine. Right now we're getting an error because I believe I don't have something created, but list is working fine because I do have everything being listed. So that is working like we expect. These are public routes while these routes are private and you need a valid API key to access them. If I were to create a new API key or I fix my API key that was there before, so let's just log in my user. There we go. And let's create an API key and now let's try to do a create and now you can see that that's working. You can see here that my Let's do it one more time. There we go.
Watch from 1:29:26And I can do things like list and everything is working and all those other things are just like I expect. Now the next thing I want to work on is a table for storing all of my books. Just to show you that now once we have this groundwork in place, it's really easy to add more stuff to our application. So let's create a new schema for books. I'm just going to paste this in because it's relatively straightforward stuff. We have a title which is a string. We have a description which is a string, a publish date which is a timestamp, page count which is a number, our author ID is referencing the author table, and we're also referencing which user created this particular page as well. So now we know which user added this book to our database and then of course our created at just like all of our other tables. Inside of our schema, let's make sure that we have our books inside of here. And our relations, we need to add our relations for our books and our authors. Now the author one is quite easy. You can see here for our author table, they have many books. So we're just going to have a many relation just like that. While our book table is going to be quite a bit more complex because our books are going to first of all have a one relationship with an author and they're having a relationship with that added by user. So I just have those in there as well. It's the same thing we did for our API keys and our users. And then finally, our users have a books added. So we're going to say r.many and they're going to have many book table values. There we go. So that's just hooking up all of our different relations between these different things to be able to be used inside of our queries that we want to do. Now before I forget, let's make sure that we run this in our database.
Watch from 1:30:43So, let's come in here. We're generating our different migration files, and then we're going to migrate all of this into our database. That way, before I forget, we have all of our database tables in place. Next, I'm going to copy my author route and call this one book because this is going to be very similar between the two. And I want to come in here with the books. And we're going to have our book routes. And let's just make sure I copy this down. Book routes and book. There we go. So, that's setting up our different API routes. And now, let's go inside of here and work on what we want. First of all, we'll need a create book schema, and we're going to need an update book schema. Now, for these schemas, it's same old stuff we've typed before. So, we have a title, which is a string that has a minimum value of one, an optional description, an optional publish date, an optional page number, and then finally, the ID of the author associated with it. And for updating a book, it's pretty much a bunch of the same exact stuff, but we've essentially made sure that they're nullable and optional for all the different values, unlike create, which is required for everything. So, now, for our get function, we want to get the books, so we can come in here with a query on our book table to get all of our books. And I also want to get the authors, so we're going to say with author and set that to true. And that's going to give me all the information for the author as well. And now, for my get function down here for an individual one, essentially, it's the same thing, but I want to do this with my author as well.
Watch from 1:32:04So, we're going to say with author of true, and make sure this is my book table. And we'll say book not found if we can't find the particular book. And we'll call this book. Just like that. So, now, let's go into the more complicated ones, which are going to be our create. So, let's get our create book schema in here. There we go. And we want to get all of our data from there, so you can see that's fully type safe. And we also want to get our user that was creating this. So, we're going to say const ID, we'll rename that to user ID, is equal to c.get. And we want to get that API key user. That is going to give us that user ID information. And we could also, if we want to get the email and role, but we only care about the ID for this particular use case. Now, the next thing that I want to do is I want to make sure the author they're trying to add for the author of the book actually exist. So, we can say const author equals await db.query the author table and I want to find the first one where the ID is equal to my data.authorID just like that. And if for some reason I don't have an author, so if my author equals null, then I want to return some type of error. So, we'll say c.json error author not found. And let's just send along a generic 400 error letting them know there is some type of issue trying to find that author. So, now we have the author, know that it's valid. Let's go ahead and create the book.
Watch from 1:33:20So, we'll say db.insert. Make sure this is asynchronous. We want to insert into that book table just like that. We want to set the values specifically. And what we can do is we can take all of our data, pass that along as is. And the only thing we need to do is pass along the added by ID, which is the user that is creating this. So, we'll say added by is set to our user ID just like that. Now, here what we can do is we can just return the full object and that gives us our book, which we can return right here. Give that a quick save and that should be everything for this create section entirely done. I want to essentially copy this because our update is going to be very similar. So, we'll come down here. Let's just say update book schema just like that. We want to get our role because I'm going to limit what can update what that's being supplied. And then here, I want to first again check to make sure our author exists. So, we'll say if our data.authorID is not equal to null, that means we actually have an author ID, we want to check it. So, we'll get our author and make sure that our author is not equal to null. If it is, we'll throw an error. Next, I want to make it so that admins or can update any user book that they want, but users can only update books that they've created. So, we'll create a simple where clause here. I'm going to say that if the role is equal to admin, then they can update whatever they want. So, they can update anytime the book is equal to whatever the current ID is for the book. So, let's make sure we get the ID.
Watch from 1:34:43That is going to be coming from our C.request.params of ID. That's that ID property up here. So, where the book table.id is equal to our ID, they can update that just fine. If it is a user though, they can only update things where the book ID is correct. So, we'll make sure that's in there. But, also where their ID is the same as the added user ID. So, let's make sure we come in and do that as well. So, we're going to say equal book table.user, I'm sorry, added by ID is equal to the user ID. There we go. So, this is just making sure that if it's an admin, they can update anything. But, if it's not an admin, they can only update the book if they also created that book themselves. And now here, we just want to do an update. So, we're going to say update our where clause is just going to be that where clause we created. And then we want to specifically set values for all of our data. And we don't need to worry about this added by section, so we can just pass along our data as is just like that. Now, the only reason I'm getting an error here is cuz I just need to move this down right there. That's going to be updating our book. And if our book is equal to null, then we just want to return some JSON that says book not found. Give that a 404 error just like that. And then we can return our book right here. Don't need to worry about passing a status cuz it'll use the default of 200 status. Finally, we're moving on to delete, which is going to be really straightforward. We're just coming here with our book table or our book table ID is equal to that.
Watch from 1:36:02And that should be everything right there for delete. But, actually I forgot. We want to make sure again we use the same restriction we did up here for our where clause. So, I kind of want to actually copy everything I have here, bring it down into our application. Let's just put it right below our ID here. I want to make sure we don't need to worry about checking the author, but we do need to get our where clause properly. So, here I want to add this where clause directly into there. That way, only users that have access to all of the books can delete it. Or, if it's your own book, then you can delete it. So, now let's go set this up inside of our Questly. So, what we can do is we can create a brand new collection. This is going to be for our books, just like that. And inside this books collection, when we give that a save, we can add in all the different requests we want. So, let's do a request for a list. And we know that our list is going to be pretty similar to what we did for authors, so I might as well just copy that over. Go to where we had that one opened. We'll just create a brand new one, I guess. Call this list. There we go. Paste that in there. This is going to say books, just like that. Give that a quick save. And if we send, you can see we're getting an empty array, which is exactly what I would expect. Next, I want to do a response. This is going to be for a get of a single value. So, let's just make sure we do that. Give that a quick save. We'll copy over what we are doing for our list. Paste this in here with a specific ID.
Watch from 1:37:28And we'll use this last ID value, just like we've done with everything else. So, here last ID, just to make sure all that works properly. And let's make sure that we save all these values. So, now inside of our get, if I were to send this, we're going to get a 404 cuz we don't have any last ID yet, which is kind of what we would expect. Now, let's go ahead and come in here with create as well. So, we'll create a brand new request. Call that one create. This one is going to be a post. Give that a quick save. Copy over the URL. Just like that. And we need to pass along all of our different JSON body information that we need. Now, we actually have quite a few values here. For example, title is the very first one. And we can just do like some random string or something along those lines, or maybe it's going to be like random lorem. There we go. Let's just do a random lorem word. That'll work perfectly fine. We're going to come in here with a description. There we go. This one is going to be random lorem words. Sure, doesn't really matter. We'll come in here with a publish date. Let's just do random date past. Perfect. Page count.
Watch from 1:38:45Random integer. And then finally, we need our author ID. And for that, we can just essentially hardcode a value. So, let's just get an author. We'll just list out our authors. And actually, we need to make sure we have our API key. So, let's make sure that we just register an account. We'll do a quick login. We'll create an API key, and then we'll come over here and make sure that we just create a brand new author, and we'll just use their hardcoded ID. That's going to be perfectly fine for our use case. There we go. And our integer is a number, so let's make sure we don't wrap that inside of quotes. That should be everything we need to do to do a create. So, if we send this along, you can see it's actually creating that, and it's hooking it up with my current user ID that I am logged into, which is exactly what we want to make sure happens. Now, let's do update next. So, what I'm going to do is I'm just going to duplicate this one. We're going to call this one update. So, we'll make that a put. Update, just like that. Give that a quick save, and make sure inside the body everything is set up. And for our title, we'll just put new in front of it, so we know that this is the newly updated version. So, now if we just give that a quick save and send, we're getting a 404, and that's because I haven't set up my post scripts yet. So, let's go in here for my post response script, set that up on my create. There we go. Last ID is coming from my response. Make sure this is a post request script.
Watch from 1:40:01Oops. Let's just copy that over again. There we go. And go into the post request and send that along. There we go. That should get our ID set on our book value right inside of here. And then when we do our update properly, we should be able to say ID and set that equal to our last ID. So, now if I just do another create here and now I do an update, hopefully you can see it's working. It does look like it's giving us our data and a success of false invalid input expected date received a string. That's probably because of our published data or something like that. Let's just not update that, so we'll just remove that so we don't even need to worry about that particular section. Let's try sending that along and now that does work. Now let's actually look at our code to see why that did cause a problem with our date. I think it's just cuz I'm not coercing my date, so let's scroll all the way up here. You can see this published date. This should have a coerce on it and same thing with my page count here. Actually, that's fine. But just the date does need to be coerced. So, that was why I was running into issues there, so that solved that problem. Let's go ahead and finish this up by adding in a delete section. So, let's come in here. We'll create a brand new HTTP request. Delete. Make sure that we save this. We'll copy over this URL. Set this to delete. Paste down that URL. And we just want to make sure we have our ID.
Watch from 1:41:17And our last ID. Get that a save. And now if I send that, we should have deleted that, which is exactly what we wanted. So, now let's make sure all of our admin related stuff works like we expect it to. Let me create a brand new book. So, I've created a book. The title is TOT. I have an ID associated with that. I'm just going to copy over that ID. I'm going to manually hard code my ID here as that value that I just created. Now that I've saved that value, I want to register as a completely different user. I want to log in as that user. I want to create an API key for that user. So, now I'm logged in as that completely different user. And what I want to do is I want to see, can I update that book that was associated with a different user? When I click send and we look at this, you can see we get book not found. That's exactly what I would expect to get cuz we want to return that we couldn't find something if the user does not have access to that particular book. So, this is working like we expect, but if this user was an admin user, they would have access to this information. We can relatively easily test that by coming into here and we can run NPX Drizzle Kit Studio. That's going to allow us to essentially modify the data inside of our database directly. We can come into here. I can find that section for wherever we have our users, and I want to get whatever my most recently created user is. So, let's just filter by that particular row. You can see that all the way down here is our most recently created user. I'm going to set the role here to admin. Give that a quick save, and hopefully just by doing that, that is going to actually make it so we can work with this data. So, let's open up Requestly here. I'm going to click send.
Watch from 1:42:33And you can see we're able to update that book because we're now marked as an admin user. So, all of our data is hooked up properly. Now again, if you enjoyed this Requestly tool, I think it's absolutely amazing and a great replacement for Postman since it's so much lighter weight and doesn't have nearly all the bloat. It's going to be linked down in the description below. It's entirely free for you to use. And I want to send a huge thank you for them for sponsoring this video. With that said, thank you very much for watching and have a good day.
Watch from 1:44:00Full transcript
- 0:00
In this video, I'm going to show you how to set up a fully type-safe Hono API that's production ready. And we're going to be covering things like every type of authentication you would need. For example, we have email password-based authentication. Also, when you log in, it's going to be based on JSON web tokens, which can then be used to create various different API keys. And these API keys are all being hashed and secured properly inside of the database
- 0:21
we're going to be using inside this project. And then we have basic create, read, update that have various different permissions for what you can do based on if you're logged in or not and based on your different admin or non-admin levels. This is going to give you that baseline of everything you need to know to work with Hono inside of an API, and it's all going to be type-safe and production ready. And if you're curious what this amazing tool is that lets us do all this API testing fully automated
- 0:43
and incredibly easily, this is the Requestly API client. It's an entirely free API testing tool that makes testing local or production or even staging APIs incredibly easy. They were kind enough to sponsor this video. And if you're familiar with a tool like Postman, Requestly is essentially a very similar tool, but it cuts out a lot of the extra bloat and things that annoy me personally about Postman, and it's just
- 1:03
a better alternative. And again, it's entirely free. There'll be a link down in the description if you want to check them out. Welcome back to Web Dev Simplified. My name's Kyle, and my job is to simplify the web for you. The very first thing we need to do to get started is just to create a Hono application. So, we can type in npm create hono@latest. And if we just put a period afterwards,
- 1:24
that'll create it in our current folder. And this will allow us to create a Hono app. Now, for the templates, it really doesn't matter too much which one you choose. This kind of depends on where you want to deploy. In our case, we're just going to be choosing Node.js cuz that's like the most basic one that there is available. And we'll say that we do want to install the dependencies using npm. And it's going to clone down just a very basic starting code. As you can see, there's really not much. If you look inside of our source, you can see
- 1:45
we just have a very simple file right here that just starts up our server and serves that content. And the only thing that's being served right now is this route that is the home page. And it just gives us the text of hello Hono. And we have a basic TypeScript config because again, all of this is going to be purely type safe. In our package.json, you can see there's pretty much nothing in here other than a dev script to start up our application. So, let's run that by
- 2:06
saying npm run dev, and that's going to start up our application. You can see here server is running on this localhost:3000. And if we just open that up, you can see it just says the text Hello Hono. It's quite small and difficult to read. There we go. You can see that text Hello Hono. Now, what we need to do is obviously turn this into an API. We don't want to be returning text inside of this. We want to be returning JSON and so on. So, the next step is to turn this into an API. And
- 2:27
the first step I really think I want to work on is just to be able to work on the most basic fetching of different data, and then we can move on to adding in our database and authorization. Now, inside of Hono to create different routes, you can say app.get, or we can do app.post, delete, put, and so on. But, we generally want to structure these routes inside of different folders to make them easier to work with because
- 2:47
if we put all the routes for our entire application in this one file, it's going to become quite large and unwieldy. So, instead, I'm going to create a brand new folder. This folder is going to be for all my routes. Let us call this routes. And inside of here, we'll create a folder for all of our author-related routes. We're just going to call this author.ts, just like that. And now, all of our author routes, for example, this route right here, are going to go directly inside this folder. And to
- 3:08
create nested routes, all we need to do is just generate a new Hono app. So, we can say const app equals new Hono, just like that. And if we properly import Hono, now we have the entire Hono app working, and this is just a nested route. So, now what we can do inside of here is instead of saying app.get, we can say app.route. We can pass it along what the path is going to be. So, for
- 3:29
example, {slash}author. In our case, let's make it {slash}authors, just like that. And then we can import the different routes we want to use. So, these are going to be our author routes. So, we want to make sure we export those here. So, we're just going to say we're going to export our app, and we'll set that as a default export, just like that. So, now I can import that author routes up here by just saying import author
- 3:51
routes from and I want to get inside that routes folder /author just like that. That's going to import those routes. And if I just make sure to put that .ts extension at the end of that, you'll see it gives us a really small error. This is something we can fix in our TypeScript config quite easily. We can just come in here and use this rewrite relative import extensions, set that equal to true, make sure I just wrap
- 4:13
this in quotes, and all that's going to do is it's going to take any place that we have this .ts extension and convert it to .js for us. So now everything is working like we expected it to. And now we have this /authors route, which is just rendering out this text. We'll just say authors. So now if we go to Hello World and we go to the /authors route specifically, you can see it prints that out. And the nice thing is is we can nest this. So this is /authors, and if
- 4:34
we want to make this route slightly differently, for example, we want to say authors two just like that. Now you need to go to /authors/two since they're going to stack on top of each other, and you can see it prints out that exact text just like before. Now before we start hooking this up to a database, I just want to have some placeholder data that we can use. So we can say const authors is equal to an array, and inside of here we're just going to put in some random IDs, and
- 4:56
we'll just give them a name. So for example, Kyle, and we'll copy this so we have two authors in this section. Just like that. We'll have Sally, and this one will be an ID of one and an ID of two. And now I want to create all of my different create, read, update, and delete routes. So the very first route that you're going to have instead of a normal REST style API is going to be your list route. This is going to list all the different things that the user
- 5:16
has access to. So we want to return all of these authors. And we're creating a JSON API, so we want to return this data as JSON and not as text. So here with that C value, which is all of our context for Hono, we can just call .json and pass it along any JSON object we want, and it's going to return that data as JSON to our user. So now we've created a list route that's going to
- 5:37
return all of that data as JSON. Now again, to just really quickly test that, we can open up that server, we can go to /authors, and make sure I spell that properly. There we go. And you can see we've got printed out an array of all of that different data as JSON just like we expected it to be. Now, the next thing you're going to create in a CRUD application is the ability to get an individual author from our list. So,
- 5:57
instead of just a list, we want to get an individual user. To do that, we can use that same app.get cuz we want to use a get method here, but instead of getting all of them, we want to get specifically an ID. And to get a dynamic parameter, so for for example, I could pass along /1, /2, /3, and so on, we can put a colon in front of whatever our variable name is and then write out our variable name, and this will
- 6:17
automatically add type safety for that variable name inside of our parameters. For example, I can say c.request .param, and that's going to give me all of my different parameters. And I specifically want to get just the ID, and you can see here I already get full type safety. So, I'll type in ID just like that. And I can come in here and now I get type safety that this ID is going to be
- 6:38
a string value because Hono is smart enough to understand all of this automatically, which is why I prefer Hono over something like Express. Next, what we just need to do is filter our list to get the author with that particular ID. So, what we can do inside of here is we can just say authors Let's actually say we want to get an author, which comes from authors. We want to filter. Actually, we want to
- 6:59
find specifically just an individual author where the ID is equal to our ID just like that. And then we can return that author down to our user. And if we didn't have an author, we could for some example throw down an error. So, we could say if our author is equal to null, that means we don't have an author. Instead, we can just return down some
- 7:19
type of error. So, let's come in here with an error that says author not found. And we can also throw along a status code of 404. So, the second parameter of this JSON function allows you to change the status code, and the HTTP status code 404 just means not found. So, it's just a little bit more information we're sending down to the user. So, we're either sending them the user or the
- 7:39
author in our case or we're sending them down an error. So, again, let's just test that out by opening this up and manually doing some testing. So, we can say {slash} author {slash} one, and we should hopefully get the first author, and it looks like it's not returning that to us. Let me just make sure that all of our code is correct. authors.find, it should be finding it. It is at that {slash} ID, so
- 8:01
that should be correct. Let's just refresh this one more time. And actually, the reason this isn't working is it should say authors {slash} one. There we go. So, you can see it's returning the first author. authors two returns the second. And if we pass in an ID that doesn't exist, you can see we get that error author not found, and it's returning the correct error codes and everything else. Now, that's pretty much as far as we can get testing using just our browser. It to do further testing, we're going to
- 8:22
need to use the requestly client. So, I'm going to just go ahead and start getting set up on that so we can do the testing for our post request, put request, and delete request, and also make this type of testing much less manual. So, inside of requestly, when you first get set up, you're going to have a completely blank environment like I do. So, the very first thing we want to do is to create a new collection, and this is where we're going to store everything for this application, because if you're working on multiple
- 8:43
applications, they're all going to have their own collections. We'll call this library, because that's a relatively self-explanatory name for what we're doing, and that essentially gives us this nice little library folder right here. Now, inside this library folder, I also like to add additional collections. So, we're going to add a collection into here, and this is going to be for all of our author related stuff. So, we'll add the authors into there, and then we can add individual requests for what we want to do. For
- 9:04
example, this is going to be for listing out all of our individual authors, so we'll call this list. Now, you see automatically, by default, it shows this get request, cuz we're doing a get. We'll leave it as a get, cuz that's what we want, and we can paste in our URL here, which is just going to be localhost 3000. Make sure I have my slashes up here, and we want to go to {slash} authors.
- 9:25
And that's going to get all of our authors when we send along this request. And if we just save that and click send, you can see we get a list of all of our different authors being sent back to us. We can also create a request that's specifically going to be for getting an individual author. So, we'll come in here, we'll create a brand new one. This is going to be called get just like that. This is going to be forgetting an individual element and we kind of want to copy this from our list. So, we'll
- 9:46
copy that over, go into our get section just like that. And this is going to be for a particular ID. Let's in our case say the ID is going to be one and then we'll give that a quick save. And if we send that along, you can see we get the user the ID of one and here you can see we get the user with the ID of two. Now, it's generally not ideal to hard code our values up here. So, using that same colon trick, we can say colon ID and that gives us these path variables down
- 10:06
here at the bottom where I can specify, for example, one, two, and so on. So, if I put in one, I can send this and get back the data for user number one. This again just helps me a little bit with my testing and I'm going to show you later how we can automate almost all of this to make it incredibly easy. But, the first thing we need to do is actually create the post method to create authors and so on. So, let's just give that a quick save and let's work on our post
- 10:26
method to create an author next. I'm just going to copy all of this code so we kind of have a good starting point. I want to do a post and in order to post, generally what you do is you post directly to that main {slash} authors route or {slash} books or whatever it is. So, we're just going to post to that main route just like that. So, we don't have an ID or anything like that. We just want to pass along all the data for the author that we're going to be creating and that comes from the body.
- 10:48
Now, we want this to be type safe. So, we're going to be using Zod for all of our validation. So, let's make sure we install Zod as a library dependency. We can say npm i Zod and we're also going to be installing a library from Hono called Hono {slash} standard validator. And this is going to allow us to use Zod to validate these inputs automatically for us so we don't have to manually
- 11:09
write that code ourselves, which is really handy. Now, what we can do is we can use this S validate function. There we go, S validator. This S validator is going to take in a string of what we want to validate. In our case, we want to validate what our JSON input is. So, we'll paste in JSON that's coming from our body, the JSON body. And then we need to pass along whatever our schema is going to be. So, we can just call this create author schema or something
- 11:30
along those lines. And let's create that create author schema. So, at the very top here, we'll say const create author schema equals z, that comes from Zod, and we want to create a brand new object for all the keys and values we want to test. Now, our schema is going to be very basic. We're going to come in here with a name. This name is going to be a string if I can spell properly. There we go.
- 11:51
With a minimum value of one. And we're also going to specify a birthday. Just like that. And what we're going to do is we're going to use z.coerce. That's essentially going to take the type of string, which is what it normally is, and try to convert that into whatever type we specify next, which in our case is going to be a date. And we're going to say that the birthday is entirely optional. There we go. Now, if I give that a quick
- 12:12
save, that's our full schema, and that'll give us all of our information for our author down here that we can use. And if we want to, we could even put a birthday on some of these. So, we can come in here. Let's say this one is going to have a birthday. And we'll set that equal to just whatever the current date is, just like that. Now, to get all of that validated JSON data, what we can do is we can say const data is equal to c.request.valid.
- 12:33
That's going to ask us what valid data we want. In our case, we want to get the validated JSON data. If we hover over the type for this, you can see that matches the type that we created in our Zod schema. Again, everything is fully type safe, which I really love. Now, the next step is to just save that author inside of our list of authors. So, what we can do is we can just say authors.push.
- 12:54
And we want to push in a brand new author. So, we'll say ID is just going to be crypto random UUID. And we can pass along all of our different data, just like that. So, it's essentially creating a brand new author. And we can return what that brand new author is. So, let's actually put it in a new variable. Just like that. We can push that author into our authors
- 13:15
list, and then we can return that author down to the user. And we're going to pass along the status code of 201, which just stands for created, saying we created this brand new user. So, now we just gave that a quick save. Let's make sure we test this out. So, let's only bring over that Requestly API client. I want to create a brand new request right here. And this request is going to be specifically for creating, so we'll call it create just like that. It's going to
- 13:37
be a post request, and we're going to be going to that URL, which is just our localhost URL. And we specifically want to go to the author section, just like that. And now we want to be able to pass along JSON data. So, we can do that inside of our body. If we specify our body type as JSON, we can now pass along any key value pairs that we want inside of this
- 13:58
section. So, let's go ahead and we specify a name for our user. Let's just say that the name is going to be Kyle number two. And we can pass along a birthday if we want. For now, I'm just going to keep it as a name. Now, if we just give a quick send along on that, you can see that we now have a brand new user being created. And this user, for the author, sorry, has an ID being specified, and it has a name. And
- 14:18
if we copy this ID, we can actually use this ID inside of our get function. So, down here, pass in that ID. And you can see when we click send, it's now returning us the information for that user, which has an ID and a name. And we never gave them a birthday, which is why no birthday shows up at all. Now, let's just make sure that we save both of these. And now, the next thing we can work on is going to be update and delete. So, let's come in here with a
- 14:39
simple update function. We're going to be using put for all of our update methods. So, we'll just say dot put like that. And in order to update, we need to update a specific ID. So, we're going to request a specific ID. Again, we want to validate the information that's being passed in. And this is going to be a brand new schema called update author schema. It's going to look very similar to our create schema. So, we can come in here, we'll copy that down. Really, the
- 15:00
only difference is we want to make sure that all of our options are optional, cuz we can sometimes update one thing and not other things. Also, I want to be able to have the option to remove a birthday. So, I'm going to specify this as nullable. This just allows me to pass along null as an option, so I can now remove the birthday if I want it as well. So, that's my update schema completely done. I also want to make sure I can get my ID inside of here. So,
- 15:20
we're going to use that exact same ID code. So, now I have my ID, I have my data that I want to update. All I need to do is get that author and update all the information for that user or that author. So, to get our author, that's relatively simple. We can just go through our authors list. We want to find the author where the ID is equal to our ID. And again, if we don't have an author, we'll throw the same error we had before. So,
- 15:41
this error just says, "Hey, we don't have an author. Throw a throw a 404 error cuz we can't actually do this." Next, what I want to do is I want to update this author's information so that it's stored properly. So, we can just say that the author.name is going to be set to data.name and the author.birthday is going to be set equal to data.birthday. And since these values could technically be undefined, I'm just going to do a quick
- 16:02
check that if our data.name is equal to undefined, I'm sorry, if it's not equal to undefined, then we're going to set it. And I'll do the exact same thing if our data.birthday is not equal to undefined, then we'll set that value properly. And I'm getting a little bit of a type error just because up here, I just need to specify the type for my author. So, we'll say that this author.type is an ID
- 16:22
with a string, a name with a string, and it's going to have a birthday that is a date, and it's going to be an optional date. And this is just an array of that data. That should clean up my type errors down here. And actually, birthday I need to make sure it can be a nullable value. So, I'll just come in here with null, just like that. There we go. Now, that does set up everything properly for my
- 16:44
author. I'm pushing that back into the list, which I don't even need to do cuz it's already there, and that should update my user and return that user's data back to us. Now, a lot of this is kind of hacky cuz we're just doing this in memory. We're going to be converting over to a database very soon after we get the rest of these methods in place, but for now, let's go ahead and actually set up our requestly for this put method. So, let's bring that over, and inside of here, I want to create a brand
- 17:04
new HTTP request. This one is going to be for our update, so we'll call this update. It is a put method, and it's going to go to essentially the exact same URL. We want to make sure it has the ID parameter at the end of it. For now, let's just hardcode the ID of one into there for what we want to update. It's going to take in a JSON body, and we can pass along any key-value pairs
- 17:24
that we want. For example, we could say the name here is going to be new Kyle, because we're updating that name to be whatever our new name is supposed to be. And now, if I click send to send along this data, you can see that it's updated that name for this first identity here to be new Kyle instead of what it was in the past. So, with that done, let's just give that a quick save, and now let's move on to the delete method next. So,
- 17:45
I'm going to copy all of this code cuz it's going to be very similar. Delete is going to take in an ID, but we don't have any JSON data we're validating, so we can just completely remove that parameter. And these parameters that we are passing along as like the second option here, this is just middleware. So, this is just code that runs before your actual request, which is this final function, and can do different things by adding or changing your parameters. In
- 18:05
our case, it's just validating this JSON data for us. So, now down here we have delete where we're getting in our ID, and all I want to do is I just want to remove that ID from our authors list. Now, to do that, we need to find what the index is that we're removing, so we can say index is authors.findindex where that ID is equal to the ID. And if we don't have an index, in our case, if the index is equal to
- 18:27
-1, that means it couldn't find anything, so we'll return a not found message here. Otherwise, we're going to go ahead and we're going to remove that from our list, so we can say authors.splice, and we can say that we want to start at whatever our index is, and we want to remove one element. That's going to remove one thing from our authors list. And then, for returning data, we don't really need to
- 18:47
return anything because we're just deleting data. We're not adding or creating or updating. So, here, I'm going to pass along a body that is null. That's a completely empty body. And to say that you have successfully executed and you have no data to return, use a 204 status. This is very commonly used with any type of delete that you're doing. Also, since our status up here is not for creation anymore, this is an update status, I'm just going to leave
- 19:08
it as a normal 200 status to specify we create or we updated this properly and 204 is being used for our delete status right here. So now let's go ahead and set this up in requestly. Let's come in here. We want to create a brand new request. This is going to be a delete request with a very similar URL except we want to have the ID we're going to be up or deleting. Let's just say we're going to delete ID number one. And now
- 19:28
it should be if we send this request along that it's passing us along no data at all. But if we test our get function right here, you can see that ID one has been completely removed from the list. You'll also notice none of our other elements are in the list. That's because every time we save our server, it restarts and refreshes all of our database data. That's because we don't have anything stored in a database yet. But that's the next thing we're going to
- 19:49
be working on once we clean up our requestly set up a little bit to make it even more automated than it currently is. So let's just go ahead and save this. Now that we have that saved, let's actually make sure we call it delete just like that. We'll give it another save. And now I want to work on highly optimizing what this is going to do. So the first thing that you'll notice is inside of all of our URLs, we have this HTTP localhost 3000 being copied between every single one of them. This is
- 20:11
obviously not ideal. And if we wanted to test our production or staging environment, we would have to manually override this. This is why there's something called environment set up that we can use. So let's create a brand new environment. We're going to call this our dev environment. And we can create any variables we want inside of here. For example, we can create a variable called base URL and we can give it a value. Let's give it an initial value of
- 20:31
localhost 3000. This initial value is synced between all of the different people in your workspace. So for example, if you have other people working on this project, this will be synced with them. While the current value is just something local to just you. For our purposes, it doesn't really matter. We'll set the same value for both of them. Then if we give that a quick save, we now have this base URL variable we can use in development. I also want to create a production
- 20:52
environment as well. And let's come in here with a base URL just like that. And for this, we would have whatever our URL is. So let's just imagine this is example.com. You would come in here and all of your example.com stuff would be there. So now we have a development environment and a production environment that both have the same variable. And we can make sure we select our development environment
- 21:12
right here. And now we can use that in all of these methods. So, let's replace this entire URL with that variable. To do that, you just open up double curly braces. You can see we have tons options. Specifically, we want that variable we just created. And if you hover over it, you can see what the exact value of that variable is. And when I send along this request, I still get all of my data being returned to me. Let's just make sure we save that. And I want to essentially copy this base URL
- 21:34
to all of our different requests. So, let's paste it down there. Give that a save. Paste it down here. Give that a quick save. Same thing for our list. Save that. And then finally, we want our update as well. And we'll give that one a save as well. Now, this next feature I want to show you is one of my favorite features in all of Requestly cuz it makes automating your tests so much
- 21:54
easier. For example, one thing that's really annoying is let's say I want to create a brand new author. Let's say I want to give it the name of Kyle 2. I click send. I now have that brand new author. I now need to take this ID. I need to copy that ID. I need to move it into delete, paste it down here. Or if I want to get it, I need to paste that value into here. Same thing if I want to do an update, I have to paste that value here. I actually have to move all over the place using the same value
- 22:15
everywhere that I go. This is kind of cumbersome and annoying to work with. This is why it's really nice to be able to create a variables. In our case, we're going to go into this author section. We're going to create a variable called last ID, just like that. We'll give it a save. And this is actually set on our authors collection, which means everything in this folder has access to this last ID variable. And we can automatically set this last ID
- 22:35
variable to whatever value we want. For example, when we do a create, I can just get this ID value right here and automatically set it. By using these scripts, we have pre and post request scripts that essentially, before your request goes off or after you get back your response, you can run various different code. In our case, I specifically want to run this line of code. So, we're just wrapping our line of code in try-catch. And we can use RQ,
- 22:58
which stands for Requestly. We can access various different things. For our case, we can access the collection variables. These are the variables set on our folder. In our case, our last ID is on our authors folder. Then what I can do is I can set that value by saying {dot} set, pass it in the name of my ID or my variable, which I called last ID. And if I want to get the value, I can use rq.response.
- 23:19
That gives me what is being returned from my API. So, that's all of this information over here. I want to convert this to JSON cuz we're getting JSON data, and I want to get the ID property right here. So, now whenever I make a request, it's going to run this code after the response gets back. It's going to take the ID, and it's going to save that ID directly into this variable right here. And then I can use that
- 23:41
variable everywhere else that I want. So, let's give that a quick save, and we'll try sending this to make sure that it works. And I gave it a quick send. You saw that we got no errors at all. And if I go into this author section, right now there's no value being set inside of here cuz it's not quite showing up. But the nice thing is if I try to use this value, it should work. For example, I can come in here and I can say that I want to use that last ID variable. You can see the
- 24:01
current value is actually set to the value from my latest create request right here. So, it did work properly. And now if I were to do a delete request, it should delete that user. When I click send, you can see no data is returned, and it did delete that user. I can do the exact same thing down here. So, I can say last ID. Just like that. Do the exact same thing
- 24:21
for my update as well. Change this to last ID. There we go. So, now any single time that I tried to make a request to create something, for example, I send this along, it's going to take this ID, it's going to save that ID, and now I can, for example, update that user by sending along the request, and it's going to make sure to use the correct ID. Or if I want to get just that user, I can send
- 24:43
this along and get it. Or if I want to delete that user, I can delete it, and I don't have to worry about copying around any IDs. Everything's automatically handled for me. We can also make it so that our data is much more automated. For example, right now I'm hard coding this name. I don't want to hard code the name. Instead, I want to use some random data. The nice thing is they have a bunch of random data you use. So, I can, for example, get a random name. Let's
- 25:04
just see here if we have a random username, random full name. Let's go ahead and go with that. And now, if I just give that a save and send, you can see every time I send, I'm getting a completely different random name. And since I hooked up all my IDs, for example, if I try to do a get request, it's going to give me whatever the most recent user I created is, or I can delete that user if I want, and so on. In my update function, I can essentially do the exact same thing. I can come in
- 25:25
here. I'm just going to put new in front, and then I'm going to get my random full name. There we go, just like that. And now, if I give that a save and I click send, this author is not found cuz we just deleted them. So, let's create a brand new one. And now, let's try to update, and you can see it just puts new as well as a completely random name afterwards.
- 25:45
Now, hopefully that little bit of a taste kind of shows you why I really, really love this Requestly tool, and why I was so happy when they reached out to sponsor me. Again, if you want to check out this tool, it'll be linked down in the description below. It's entirely free to use all of this, which is absolutely amazing. Now, the next thing that we need to work on our application is hooking it up to a database cuz right now, everything's hardcoded. It's not great at all. So, let's work on using Drizzle for our database. We're going to
- 26:07
be using the brand new version 1.0 release of Drizzle, which is honestly super incredible to work with. Now, in order to install Drizzle, we just need to run npm i Drizzle ORM. And I'm currently recording this before the version 1.0 is released. It's currently in a release candidate. So, if you're watching this video right now before version 1.0 releases, just type in @rc at the end. That's going to
- 26:28
get you the release candidate version. But hopefully, by the time you're watching this tutorial, or if you're watching a little bit in the future, 1.0 will currently be the standard release, and you won't need to add that @rc at the end of it. I'm just doing this to keep this tutorial as up-to-date for when Drizzle releases version 1.0 quite soon. We also need to install the development dependency, so just do a dash capital D. And this is going to be Drizzle Kit.
- 26:50
This is just making sure we can do migrations and things like that. Again, if you're watching this video right now, you need to use the release candidate version, but in the future it should hopefully be the 1.0 version automatically. Now, if we look inside of our package.json, we should see that we have inside of here DrizzleKit with that release candidate version and the Drizzle ORM. Now, we're going to be using Postgres for our database, so we need to install all of our Postgres
- 27:10
stuff by just saying npm i pg. And we can also install the development dependency of @types/pg to get all the types for the Postgres library. And finally, I want to also install .env, so we're going to install .env so that way we can get all of our environment variables working properly in our application as well. Now, to set up our database, we first of all need to create an environment variable, so we'll
- 27:31
say .env. This is going to be our environment file, and let's make sure it's included in our .gitignore, which it is, which is great. So, now we want to create a DB section, and inside of this section we're going to have a few different variables. We're going to have our DB password. So, let's make sure we come in here with our DB password. Set that to whatever you want. We'll just say password for now.
- 27:52
We're also going to have our DB user. We're going to set that to Postgres. We're going to have our DB name. We can call this whatever we want. Let's just say library YouTube video. We're also going to come in here with our DB host. We're going to set that to localhost cuz we're going to be running this locally. And then finally, our port. The default port for the application is 5432, so
- 28:14
we'll just set it to the default port just like that. That hooks up all of our environment variables that we're going to need. Now, we need to run Postgres on our computer, and we're specifically going to be using Docker for that. So, let's create a brand new file. We're going to call this docker- compose.yml. I'm going to paste in what this Dockerfile specifically looks like, but the key thing is we're going to have a services section up here with a database inside of it called DB, and then we need
- 28:36
to hook up the image we want to use. In our case, we're using Postgres version 18. Our host name, ports, environment variables, and volumes are all kind of set up based on our environment variables. You can see we're just porting over all of our environment variables, and this volume just tells it Postgres where to set your actual data, and these volumes down here just exposes that data to our computer. Now, don't worry, you don't need to understand how
- 28:56
this Dockerfile works or anything inside of it. You can go to the GitHub repository linked in the description and copy this file exactly as is, and as long as you have Docker up and running on your computer with something like Docker Desktop, it'll run perfectly fine once you paste in this configuration file, and then just run Docker compose up. That should start up your application. As you can see, everything was created and is working just fine
- 29:16
inside of our application. Now, the next step to work on is to be able to get our Drizzle configuration set up. So, inside of our application, let's create a brand new file called drizzle config .ts. And inside this drizzle config, we're just going to export a default define config. And this define config function should be getting imported from Drizzle. So, let's just make sure we do a quick
- 29:37
import define config from and that comes from Drizzle Kit just like that. Now, inside here, we can define a bunch of different things. First of all, the out location is where all of our migration files are going to go. We're going to put them inside of our source folder, and we specifically want them to go in a DB folder called migrations just like that. We then have our schema. This is
- 29:57
where our schema is going to be located, so we can say that this is going to be in our source /db/schema. There we go, .ts. Next, we need to specify our dialect. In our case, we're using Postgres, so we can use Postgres as our dialect, and we also want to set strict to true and verbose to true, and that's just going to add extra safety nets inside of this.
- 30:18
So, when we do migrations and so on, it'll tell us what code is being run and make sure if we try to run any unsafe code, it'll give us a warning before doing so. Lastly, we just need to hook up all of our database credentials. So, this is going to be things like our password, which we know comes from our environment variables. So, we could say process.env.dbpassword, but doing it like this is not type safe
- 30:39
at all. So, instead, I'm going to create an environment variable file for all of our type safe environment variables. So, we're going to create a brand new file. We're going to put this inside of data/env.ts. Inside this file, I'm going to import .env/config. That's going to load up all of these environment variables for us. And then inside of here, what I want to do is I want to create a brand new environment schema using Zod. So, we're going to
- 31:00
import Zod. And all I'm doing is I'm taking each one of our environment variables. So, DB password, DB user, DB name, DB port host, DB port. And all I'm doing is I'm making sure that they work with in all of my schemas. So, I'm just making sure that these are values that exist. And I'm also putting a port in here. So, by default, it'll be port 3000. And that's nice because inside of this file, I can for example replace this port with env, which if I make sure
- 31:22
I export this There we go. Export Actually, that's our schema. We need to get our data from that. So, let's get our parsed data, which is env schema.safe parse Pass it along our process.env. That's just going to parse all of our data. You can see here. It's going to give us all that information. And we can just say, "Hey, if our parsed data is
- 31:44
not successful, then we have an error." So, we'll just say throw new error "Invalid env." And we can just pass it along our parsed.error.message. That way we know if there's something wrong with our environment variables. Otherwise, we're going to export our env air variable, which is just our parsed data. There we go. All this code is doing right here is just adding type
- 32:06
safety to our .env file. So, now wherever I have that index file, I can say env. I can import that from that file I just created. And now I have type safety for all my different environment variables, which is exactly what I want. And in my Drizzle config, I can now get those environment variables as well. So, I can say env to get my password. I can come in here with my database. And my
- 32:26
database here is going to be env.db name. I can also make sure I get my user, which is my env.db user. Let's see, we have our host env.db.host. We have our port, which is our DB port, just like that. And then we're going to set SSL to false just because we're not working on a secure connection cuz
- 32:47
everything is local, so that's why I have that set up as is. Now, in order to use our database, let's go into our source folder, create a new folder called DB, and inside that folder, we're going to create a file called db.ts. We're going to export a constant called DB, which is just going to be equal to calling the drizzle function, just like that. And this drizzle function is going to take in an object, and we're going to be passing it in a
- 33:07
connection parameter right here, which comes from our environment variables. So, if we take a look at this, you can see all I'm doing is mapping my password, username, database, host, and port exactly like I did before. And now to get drizzle, let's import that as well. So, drizzle, that comes from our drizzle ORM. There we go, drizzle ORM, and we're
- 33:27
using the Postgres version on Node, so we're going to say Node Postgres just like that. So, now we've hooked up our database with all these different connections, and these connections right here. So, now we can properly use our database inside of our application if we want, but we need to add what our schemas for our tables and so on look like. So, let's create a folder called schemas.ts, and this is where our not ts, schemas,
- 33:47
there we go. And inside of here, let's create a folder called authors.ts to define what our author table is going to look like. So, to create this table, we can just say we want to export a const, we'll call this our author table, which is using the PG table function, just like that. And we can specify the name for this, which is authors. Then we can specify each individual key that we have. So, for example, we can have an
- 34:09
ID, and our ID is going to be a UUID coming from drizzle. We want this to be a primary key, and we want it to be a default random value. So, this is going to be our default ID that we use for everything. We can also say that we have a name, which is just text. Again, make sure this comes from the Postgres version. We want this to be not null, and then
- 34:29
finally, we're going to have a birthday. This one is going to be a timestamp. Again, make sure we get the Postgres version. We want to set that to say with time zone true. That's just going to make all of our time zone conversions much easier. And then finally, we're going to have one more field. This one is going to be called created at. And this created at field is going to
- 34:50
have be not null. It's going to be required. And it's going to be a default value of now. So that way it's always defaulting to whatever the current value is whenever we create a brand new author. Now we're going to have multiple different tables. I've just broken them out into each of their individual files. But to use them all in one file, we need to create a schemas.ts file. And in this file, we're just going to do a simple export star from and we want to go into
- 35:11
that schema folder just like that. So for every table we create, we're going to create an export line right here so we have access to that directly inside of our database. Now what we can do inside of our DB folder is we can pass along our schema just like that by importing our schema. So we're going to say import star as schema from that {dot} {slash} schema
- 35:32
TS file just like that. And that's going to import all of our schema related data directly into here. Now technically, this is the old way of doing this inside of it and we need to use relations instead. So we're going to create a brand new file called relations.ts. And this relations file is going to import all of our schema data just like this. And it's going to export a constant called relations which we're going to set to
- 35:53
some particular value. And then what we can do in our database is replace this with relations. And up here, we can import relations from that {dot} {slash} relations file just like that. Now to use these relations, all we need to do is we need to use our define relations function and pass it along our
- 36:14
schema just like that. And there we go. And as we start to add more tables, we'll define the relationships between those tables. But since we only have one table, we don't need to do anything. So this is the only thing this file has for now. And that has hooked up all of our relations with our database. So, now we can start using our database properly inside of our application. So, now let's go into our route for our author, and
- 36:35
let's just completely remove all this hard-coded stuff, and change all of our routes to work properly. So, all of these routes are going to be changed to asynchronous routes cuz we're going to be doing asynchronous data gathering. And the first thing I want to do is I want to get all my authors, which is just call await, and we're going to call db {dot} We want to query. Make sure I import that. Query my author table, and I want to find many, which is just going
- 36:56
to find everything in that list, and now we can return that data as is. So, already that's completely solved this section. Let's work on our get section next. So, here we just need to change this code to say await db {dot} query. We want to query our author table. We want to find the first one. And we can use a where clause right here to say where the ID is equal to this ID right
- 37:17
here. There we go. Make there sure that is asynchronous, and that solves this entire section. Again, we're just replacing that hard-coded data with our actual database. I can do the same thing down here with our create function. So, instead of doing an authors {dot} push, we're going to say db, and make sure we await this. db {dot} insert. We want to insert data into our author table.
- 37:37
And we want to set our specific values just like this. So, to set our values, we can come in here with an array of our {dot} values, or in our case just pass along all of our data as is. We also want to get all of our data, so we can just say returning just like that, and it's going to return all of our data. And if we make sure that this is an asynchronous function, we can then say const author is equal to getting that
- 37:59
data from here. And this is actually going to return to us an array of all the data we inserted. We're only inserting one value, so by destructuring it like this, that gives us the first author we just created. So, that's updated our entire create function. Our update function is going to be, luckily, a much easier function for us to work on. I'm actually going to copy the code right here cuz it's going to be very similar. We can just come in here, paste
- 38:19
that down. I can do a simple DB update. There we go. And we want to set our data to be this new data. And we need to make sure we restrict where we update, so we can just push a where clause directly onto here, and we can use this equal function to say where our author table.id is equal to our ID. Now, what
- 38:40
that code has done, if we make this async, is it's properly done an update function for our author. It's returning all of our new author data based on that update. And this code down here, we can just completely remove cuz that was only there for that placeholder section. So again, just replacing this code with our database code. And finally, for delete, again, this is going to be quite simple because we can just do DB.delete
- 39:01
on our author table. And we want to delete where that particular clause is set. So we'll just copy over the exact same where clause cuz we only want to delete in the instance where it matches the ID. There we go. Make sure that's asynchronous. And now we finished setting that up as well. So to make sure that all of this is working properly, let's first go ahead and migrate over our database. So what we can do is we can run NPM run
- 39:22
actually NPX drizzle kit. And we want to do a migration, so we can just say migrate. And actually, we should generate our migration first. So we'll say generate just like that. This should successfully work, and it's right now saying no schema files found for this particular path. So let's make sure that our path is correct. I called this schemas. This should probably be called schema to make sure it matches our name.
- 39:42
Now, let's just try that command again, and hopefully it should work this time. You can see it properly created our migration, which you can see right here is migrating all of our data. And now, if we just do drizzle kit migrate, that'll actually push all of that data to our database. Now, it looks like we are getting a bit of an error. I think that's because I stopped my Docker, so we can say Docker compose up.
- 40:02
That should hopefully set all this back up. There we go. I just stopped it for some reason earlier. And now, if we try to run this migrate, it should hopefully work properly. There we go. Looks like that worked. The reason I was getting an error the second time was because I had a different port being run for a different application, so it was causing an error. But now, I just closed my old Docker that was running and now I have this all working properly. You can see it properly migrated everything and now
- 40:23
if we go into our application and do the testing, everything should still work exactly the same as before, but it's persistent in a database instead. So let's just go to our client to test to make sure everything's working like we expect it to. Let's make sure we save anything that wasn't saved before. There we go. And now let's go ahead and we'll just do a simple create. Minimize that down, click send, and you can see it sent all that information and now we're getting that created at field from our database. Our ID is automatically
- 40:44
randomly generated and that birthday is set to null because we didn't give it a birthday by default. Let's try a get to see if that works. You can see it's giving us that one single user. If I do a list, it should also return only one user cuz there's only one that's been created. Let's create another one so now we have two users in our database and if we do a list, it's returning both of those users. If I do an update, it should hopefully update our user. Let's
- 41:04
give that a quick send and you can see that worked properly as well. So now if I go back into my list, you can see that that user has been updated. And finally if I do a delete, we can also remove that user. If we go to our list, they've been removed from that list. So everything is now being properly synced with a database which is exactly what we want, which means now we can move on to some of the more fun stuff which is dealing with authentication and also dealing with relationships between
- 41:25
different tables. So let's go ahead and create a user table where we're going to store all of the information for a user. We'll just copy this author table cuz it's going to be very similar. So we'll say users. This one is going to be our user table. We'll call it users and now we need to specify all the fields associated with a user. And a user is just someone that can log in and view data in our database. So they're going
- 41:45
to have an email so we're going to say that that is going to be a text which is not null and also it must be unique so we'll come in here with a uniqueness on that. We also want to pass along a password for the user. So we'll say password, but we don't want to store their password as a plain string. That's very insecure and definitely something you don't want to do. Instead we're going to be hashing that password so we'll call this a password hash. This does not need to be unique. Multiple
- 42:06
users can have the same password. So we'll just say it's text that is not null. We're also going to give the users a role. So to create a enum, which is what we're going to use, we're going to come up here with an export const user role enum. We need to set that to PG enum, and we can give it a name. We'll just call this our user role. And then we can specify an array of values. So, in our case, it's going to
- 42:27
be either user or admin. That way we can do different things with different permission levels for our users. So, for our role here, we can set it to our user role enum we just created. We can say it's not null, and by default they're going to be a user just like that. So, now we have a email, a password hash, a role. We don't need a birthday, and we have a default created at. That's everything that constitutes
- 42:47
what a user looks like in our database. And there is no relation between an author and a user, so we don't need to modify what our relations looks like. We just need to make sure here we export what our schema looks like. So, inside of here, let's export our users just like that. So, now we have both of those available in our database. So, now whenever I type in db.query,
- 43:08
I can see I have the author table and the user table that I can query from right now, and that works properly. Now, in order to set up our users, I'm going to take our author routes and completely copy them, so we have our user routes. And inside of here, we want to get our user routes just like that from our users. And we'll just say user routes comes from that user file we just created. So,
- 43:28
now essentially {slash} users is going to give me my users routes. Now, I think I'm going to rename this to off actually, so we'll just say off off routes. And we'll call this off as well. And finally here, we'll call that off just like that. So, that just kind of cleans up all that. Let's make sure this import works properly. There we go. And now inside this section, I want to convert everything
- 43:48
from this author related stuff to be working with users and authentication. Now, in my application, there is no way to get a list of users or anything like that. So, we're going to completely remove all of our get methods, and instead the only methods we care about is going to be registering a new user or logging in as an existing user. So, we're going to have a post for register, and we're going to have a post for login. All this get and delete and put
- 44:09
and update, we don't need those. We just need to be able to create and login as a user, and those are both going to be post request. So, for this register, let's create a schema for that. We'll call this register schema. There we go. And to register a user, all I need is an email and a password. Let's make sure that we set this to an email.
- 44:30
Just like that. And this one down here is just going to be a string. There we go. And we don't want this to be optional. Of course, they obviously need a password. And we can specify, for example, it must be at least eight characters long to give us a minimum value. We can do the same thing with a login schema as well. The login schema is going to be essentially identical, but we're going to make sure that the password does not have a minimum
- 44:50
requirement cuz if our password requirements change over time, having this be the least restrictive as possible is ideal for what we want. So, now we have a login and register schema. We can use that register schema here. And now what we have access to is all of our data for the email and the password. We just need to make sure we properly hash this password, store it in our database, and then later look that data up properly. Now, I'm going to create a
- 45:11
brand new folder where we're going to store all of our hashing and off-related stuff. So, I'm going to say lib just like this. And inside this lib, I'm going to create a folder called crypto.ts for all of our cryptological stuff. So, we're going to have a async function. This one is going to be called hash password. This is going to take in our password as a string, and we're going to convert this to a hashed version. And
- 45:32
essentially hashing a password, all as it does is it takes your password that looks like this as plain text, and it converts it to a bunch of random nonsense, but it's deterministic. That means the same password gets converted to the same random nonsense every time, and it's a one-way conversion. The reason you do this is if for some reason someone gains access to your database, instead of seeing everyone's password as
- 45:52
strings like this, they instead see this absolute nonsense, and there's no way for them to go from this nonsense value to what your actual password is. It's a one-way encryption, which is why it's called a hash cuz encryption is two-way. You can go encrypt and you can ecrypt, but with hashing you can only go one direction. So, we're essentially creating an irreversible version of our password, so people can't figure out
- 46:13
what it is if they get access to our database. So, that's what this entire function is doing. And the nice thing is is there's a lot of built-in functionality to help us do this. Specifically, there's a function called scrypt. So, we're going to import scrypt just like this. This is going to come from the node crypto library. And this gives us access to everything we need to do for hashing our passwords. So, first we want to create what's called a salt.
- 46:33
So, we're going to say salt. This is going to be a completely random bit of data. So, we'll call random bytes, pass it along 16 here to give us some random data. And we'll convert this to a string. And this is going to be a hex-based string. Now, the reason we use salt with our password is because, like I said, whenever you convert a convert a password, it's a one-way conversion. So, password always converts to the same
- 46:55
thing. And if I pass in the same exact string, it's always going to convert to the same string on the other end. By adding in a salt, it's just random data that gets added in at the end of our password. That way, the output data is also somewhat randomized. This is useful because, if for example, I know that converting password always gives me the value 1 2 3 4 5, if I am a hacker and I
- 47:16
get access to your encrypted or your hashed passwords, and I see the value 1 2 3 4 5, I know that that actually means the user's password is password. I can essentially do a reverse lookup because I know what the one-way conversion is. By adding in this random bit of data at the end, it now takes this thing that the hacker knows the value of, and it changes it to something else that's entirely different. They don't know what
- 47:37
the value of this is. So, this is just to help you against what's called table attack or a dictionary attack or rainbow attack. All these different terms are essentially the same thing. We're just helping make this even more secure than it was before. So, now with this salt and this password, we can create the hashed version. We're going to call await, and we're going to call scrypt async just like that, pass it in our
- 47:58
password and pass it in our salt. And we're just going to create a nice little a sync version because this s crypt version uses callbacks, which are not really ideal. We want to use promises. So, we're going to create an a sync function called s crypt a sync, which takes in our password as a string and a salt that's a string.
- 48:19
And essentially, we're just going to return a new promise that is going to wrap what we had before. So, this is going to be our request and our response. And actually, it's supposed to be our resolve and our reject, just like that. There we go. And what we can do is we can call that s crypt function. This s crypt function takes in a password. It takes in a salt. We're going to pass
- 48:39
along the number 64 here. That's how long our key is going to be. Essentially, that just is how long we want our password and encryptions to go for. And then finally, we get a callback. This callback is going to give us an error and it's also going to give us our derived key, just like that. Now, if we have an error, we can just reject that error. So, we'll say reject and pass along the error, just like that. Otherwise, if we don't
- 49:01
have an error, we can resolve the proper thing. So, we'll just do a quick return right there. Otherwise, we'll call resolve with our derived key. Now, our derived key, if we look at the type here, is this non-shared buffer. We can actually generic this type slightly by saying that it is going to be a buffer. And this buffer is an array buffer, just like that. That's the generic type that we want to use specifically for this
- 49:22
section. Now, I shorthanded this to just already have it. So, we'll shorthand this right there. And now, we have this s crypt function completely written. We have our hash value right here. And essentially, that's all we need to do. So, now what I can do is I can return that hash, but I want to make sure the salt is also part of that hash. So, I'm going to return my data as salt and I'm going to put in my hash as well. So,
- 49:42
here's my salt at the very start. And then I'm going to put a colon. And afterwards, that is going to be my hashed password. And I need to make sure I convert that to a hexadecimal just like that. Close this off and now if I give that a save, essentially what I've done is my password is now storing the salt at the beginning of the password and it's storing the hashed version of that at
- 50:03
the very end. So that way when I get my data from my database, I can just split on this colon to get my salt and to get my password data. So let's go ahead and go back into this off section here. The first thing we've done right here is we've tried to insert a brand new user into our database. Clearly, that's not what we want to do. The first thing we want to do is we want to say, is there an existing user in our database? So we'll say existing is equal to await db.
- 50:26
We want to get a query for the user table. We want to find the very first one and we want to find them where our email is equal to the email that comes in from our data. So we can come up here and say that the data we want to get from here is our email and our password. So this is going to figure out, do we have an existing user or not? If existing
- 50:48
is equal to null, well that means that we don't have an existing user, but if it's not equal to null, that means we're trying to register a user that already exists. That's obviously not something we want to allow, so we're going to throw an error. We'll just say, email already in use. And there we go. We can send along a 409 status
- 51:09
code for this one as well. A 409 status essentially means that the user's data being passed up is conflicting with certain things that we expect and hopefully they can fix that data themselves. It's very similar to a standard 400 error you may have seen exist, what we can do is we can get the hashed version of our password. So we'll get our password hash by calling that hash password function and passing it in
- 51:29
our password just like that. This is an async function, so we'll make sure we await it and then we can get our user by just calling that same exact db.insert into our user table. We want to insert specifically values and the values we want to insert are email and our password hash just like that. That's all the information we want cuz again, this password hash, if we
- 51:51
remember, it includes our salt and our password, which is what we need to essentially reverse engineer what our password is. We also want to make sure we return all that user information, but we only care about specific fields. For example, I care about the ID field and the email field, so we can say user table.email. I don't want to pass down the password or anything like that. That would be very insecure. Finally, I can
- 52:12
pass my user down just like that. Now, if I just make sure I await this because this is an asynchronous function, that should be everything I need to do to register a user inside of my database. So, let's go ahead and test this out before we do our login section next. So, to do this, we can create a brand new collection. We're going to call this collection off and inside that collection, let's go ahead and we'll create a brand new request and this
- 52:32
request is going to be for register. We know that this is going to be a post request and it's going to use that base URL. And we want to make sure that we go to off/register. Actually, we don't need any /register. It should just be /off, I believe. Actually, I believe it is register. I lied. So, let's just give that a quick save. Now, let's go ahead and set up our body to pass along our JSON to register
- 52:53
a user. We know to register a user, we need to pass along an email and of course, we can use that random email information. So, we'll say random email and we also need a password. This doesn't really matter what it is, so we're just going to set it to password just to hard code a value just like that. So, now, if I give this a save and I click send, we should hopefully be creating a user, but it looks like we're getting some type of
- 53:14
error. So, let's go ahead and actually take a look at what that error looks like and it looks like here, it's failing cuz our database table doesn't exist. That's cuz we need to re-push our database. So, let's create a new migration and then we're going to migrate all of that data into our database and that should hopefully create that brand new table for us. Once that's done, we should hopefully now be able to send this request along and actually get a user created. And if we look over here, that does look like it
- 53:35
worked. We have a user with a brand new email and we have an ID for that user. So, now we have have user created in our database, we can go ahead and we can log that user in, which is the very next step of our application. So, what I'm going to do is I'm going to come in here. I'm going to copy this entire post cuz our login is going to be relatively similar. We're going to call this login. We're going to use our login schema,
- 53:55
just like that. And of course, what we want to do is we want to get what the user we're trying to login is. So, we'll say user, just like that. We're going to try to find the first one where their email is equal. And if for some reason our user does not exist, so if our user is equal to null, we'll tell them that, "Hey, for example, this is an invalid email or password." Invalid email or password.
- 54:16
There we go. And we're going to send along a 401 status code, which essentially means that you failed to authenticate properly. Now, all the code down here we can essentially remove. And instead of hashing our password, I want to verify it. So, I want to check, is my password valid? And we can use a function called verify password, which is going to take in the password they type in, as well as our hashed password from our user. And
- 54:37
we want to check to make sure that these are the same exact password. So, let's go into our crypto function, and we're going to create a brand new asynchronous function called verify password. We know this is going to take in a password, which is a string, and it's going to be taking in a stored hash password, which is a string as well. And all we want to do is we want to compare these
- 54:58
two values. So, first we need to get our salt and our hash based in hexadecimal cuz that's how we saved it. You can remember here, our salt came before the colon, our hexadecimal hash came afterwards. So, to do that, we can take our stored hash and we can split it on the colon symbol. That's going to give us these two halves together. Next, what we can do is we can get our stored value by saying buffer.from,
- 55:20
and we can take our hash that is in hex, and we convert it away from hex. So, essentially what this little bit of code is doing is it's taking this hexadecimal string and converting it to a buffer array, which is what we need to be able to compare these two passwords properly. And that's because we're going going using a function that is called timing safe equal. And this timing safe equal function is going to take in our stored
- 55:42
value. This is the one that comes from our database. And then it's also going to take our derived value and compare them together in a way that you cannot use timing attacks against. Essentially, timing attacks are kind of fancy attack that depending on how long an equality takes, you can determine how close your password is to the correct one by using this function that gets rid of that entire vulnerability. So now all we need to do is get our
- 56:03
derived password. So we can say const derived is just equal to calling scrypt async and passing along our password as well as that salt. So what I'm doing is I'm using the exact same salt from the stored database password plus the password my user typed into the form and I'm encrypting that using scrypt. So I'm hashing that and I'm comparing it to the
- 56:23
already existing hash and I'm saying if those two values are the same, that means they typed in the same password both times. If they're different, that means they typed in different passwords. That's all this function does right here. So now inside here, we can import that function and if this returns true, the passwords were correct. But if it returns false, so if it is not valid, then we know that they have an invalid email or password. So we can send along
- 56:45
that information. And the reason I'm specifically telling them invalid email or password for both is because if I said up here invalid email instead of invalid email or password, they would know they have an email of a user in our account system. So that's generally why it's best to always say invalid email or password for these types of things. Now in order to say what user we are for our different validations in our
- 57:05
application, we're going to be using JSON web tokens. And luckily, Hono makes it really easy for us to do that. So we can use a function called sign. This is coming from Hono. It's an asynchronous function. And this function just allows us to sign a JSON web token. Make sure we import that from Hono/jwt. And with this sign function, we can pass a lot along a lot of information. We're going
- 57:26
to pass along an expiration data for how long we want this to work. First of all, we need to pass in what our current time is, so we can say math.floor of date.now / 1,000. That's going to convert the current time to the correct format that is expected for this expires time, and then we can just set a default expiration. So, we can come up here and
- 57:46
say const JWT expiration in seconds. And we can specify how long we want this to be. So, if we say like 5 * 60, that would be 5 minutes. And that would be how long our JSON Web Token last. So, essentially by coming here and saying now and we add along what that expiration time is, we're
- 58:07
saying that this expires 5 minutes in the future. So, our JSON Web Token last for 5 particular minutes. Now, the next thing we can do is pass along any data we want. For example, we can pass along the subject that is our user.id. And we can also pass along other information like the email. So, we'll say user.email. And now we have access to the user's ID and the email whenever we try to access
- 58:28
this JSON Web Token. And this is going to give us a token as a particular value, and we can send that token down to the user just like this. And we don't even need a status code associated with that. Now, the final thing we need to pass along to this is going to be some type of secret value. So, we're going to get this from our environment variable. And we're going to call this JWT secret just like that. So, let's go into our
- 58:49
environment variables, and we're going to create that JSON Web Token secret. This is going to be a string with a minimum value of one. And now we need to make sure in our environment variable for our JWT section we create that JWT secret. Now, there's lots of ways you can create this secret. You could just hardcode some random value, but I like to do to create this
- 59:10
secret is just open up a random terminal for Node, and I like to type in buffer.from. That allows me to create a buffer. We're going to use crypto.random I'm sorry, get random values. And I'm passing in a new Uint 8 array. And then I can specify how long I want that array to be, so we'll say 16 values. So, I've essentially created an array that is 16
- 59:32
values long. I'm converting that to a buffer, and then I'm taking that buffer and converting it to a hexadecimal base to string. If I make sure that I close off all my parentheses properly, and I make sure that I spell uint array properly, cuz this I should be lowercase. There we go. You can see that's giving me back a value. And the reason this value doesn't look perfect is cuz I messed up my parenthesis replacements.
- 59:53
There we go. So, now if I put that value there, you can see it just gives me a random string of data. And this random string of data is nice and secure, and I can use this string of data, and every time I generate this, you can see it's just another random piece of data. You want this to be as random as possible. That's why this is what I like to do to create this random data. Now, I have that JSON web token secret. That's what signs my JSON web token. So, essentially
- 1:00:14
the way that this function works inside of our off is we create a token, we sign it with this secret value that only we have, and then when the user wants to do something, they send that token back to us, and we're able to use this secret value to decrypt this code and get access to what the user's ID and email information is. So, let's test to see if this works. Let's come over here with our Requestly. Let's just duplicate this
- 1:00:35
one for register. We're going to call this login, just like that. Our body is going to use a email that we're going to paste in whatever actual value is. We're going to have a password here, and when we send this along, we should hopefully see Right now, we're not getting any data. There may have been something weird going on though, so let me just make sure I resend that. Still not quite working. Let's make sure that we actually give this a proper name of
- 1:00:57
login, just like that. Give that a quick save. And also, let's make sure that we pass it along the correct information. So, inside of our register, let's just send along a register. And it looks like that's not working as well. I wonder if our server may be stopped working at some point, so let's just go ahead and check our server. Looks like there was some errors. Let's just try rerunning that and see if that works properly. Looks like there's no more errors. That's probably cuz of our environment
- 1:01:17
variable stuff. So, now let's give that a quick send, and And can see that did work properly. We can have an email for our user. We can copy that email, go into our login, and where we have our body for our email, we can paste that email in, click send on this, and you can see we're generated a brand new token, and that token can then be used to send along with other requests and information associated with it. Now, again, to make this process a little bit more automated, I'm going to go into my
- 1:01:39
auth variables. I'm going to create a variable called last email, just like that. We'll give that a quick save. Then, what I want to do, just like I did for my post response request here, inside of my register in a script, I want to make sure that I set my last email equal to my response.json.
- 1:01:59
email, just like that. And this should be in the post response. So, let's make sure we're in the post response section. There we go, just like that. And now, if I give that a quick save, and I just click send, that should save that email inside that last email variable. And now, when I do my login, I can just use that last email variable, just like this, instead. And now, when I
- 1:02:20
send, you can see I'm getting a token for whatever the last email I used for my user was. Now that we have all this set up, we have a token that we can use anywhere to authenticate as this user without passing along an email and a password. And JSON web tokens are great for short-lived authorization, but they're not great for long-term authorization like APIs, because you if you have an API key, you want to be able
- 1:02:40
to revoke that API key, and JSON web tokens are not something you can do that with. So, what we want to do is we want to be able to convert this JSON web token into a long-term API key that we can use wherever and whenever we want. So, the next step is to be able to create API keys that read this JSON web token to make sure the user's authenticated, and then we can do whatever we want from there with that API key. So, let's go ahead and work on
- 1:03:02
API key sections next. So, inside of our database, we need to create a brand new schema, and this one is going to be for API keys. Now, you've seen me type these tables out a bunch, so I'm going to copy over what this table is and explain it in depth. You can see we create a table called API key tables called API keys. We specify an ID, we specify a name, a hash for the key, again a one-way hash,
- 1:03:23
that way if someone accesses our database, they have no way of getting what the API key is. And we also are passing along a key prefix. So, this key prefix essentially is just storing the first eight values of our key. So, if our key is just a key that looks like this, super super long, whatever it is, and it just goes on and on and on and on and on, our key prefix is just going to take the first eight values of those
- 1:03:44
keys and store it in our database. That way when a user looks at their API keys, they know, "Oh, here's the first eight values. I know what API key that is." That way they can use it without actually storing the full API key in our database, cuz we don't want to store the full thing. We don't want users to be able to access it. We're going to use the exact same hashing method we used with passwords to make this more secure. Also, API keys are going to be
- 1:04:04
associated with users. So, here I'm setting up a references that allows me to create a foreign key between my user table and my API key table. So, this is just pointing to my user table. And if I delete my user, it's going to delete all the API keys associated with them. So, now inside my schemas, let's make sure that we take our API keys and we export them. And now we have two tables that have a relationship, our API key and our
- 1:04:26
users. So, in our relations, we can actually set up what our relations are going to look like. This is going to take in a function which just returns an object, and you can see all three of our tables. So, our API key table, for example, this is going to have a relationship of a single user, so we can call that a user. This name right here is whatever you want it to be. And this user value right here, we need to make sure we call r.1, cuz this is a one
- 1:04:48
relationship, a API key has one single user. And we want this to be on the user table, so they have one user. And then we have a from field, just like this. So, this is saying what key table are we coming from? Well, we're coming from the API tables user ID, and we're going to the user table ID. So, this is just linking these two table columns together, just like that.
- 1:05:10
And I can essentially do the exact same thing for my user table, just like this. They have many API keys, so we can say API keys, just like this. This is a many relationship on that API key table. And when you have many relationships, you don't need to store any additional information. So, you can see I now have that many relationship set up. So, now we need to create a route for this. So, I'm just going to take my off route,
- 1:05:31
call this API keys or API key, just like that. And our index, we'll create one called API keys. This is our API key routes. And if we just copy this down, just like that. There we go. Now we've hooked up that route at {slash} API keys. And generally with routes, they're going to be this kebab case. So, we're
- 1:05:52
going to kebab case this just like that. And now we can work on this API keys route. And this one is going to essentially have a way to list all of our API keys, as well as to create and delete API keys for security purposes. So, let's just go ahead and actually clean this up drastically. I'm just going to delete pretty much all the code we have inside here, so we have a fresh fresh slate to start with. I'm going to do a simple get method to be able to get an API key. This is going to take in my
- 1:06:13
context, which will be asynchronous cuz we're going to be doing some database request. And what I want to do inside of here is I want to get all my API keys for my user. But how do I know what user I'm currently logged in as? This is where we access that JSON web token, and we're going to be using middleware. So, to use middleware for all of our different routes, normally you've seen us pass along middleware here, like our
- 1:06:34
S validate function. We passed that along here as a middleware for individual routes. We want to add middleware to all of the routes inside this section, we can say app.use, and we can pass along our middleware. Our middleware is called JWT, again coming from that Hono JWT. And inside this JWT right here, we can pass along our options. So, for example, our secret is
- 1:06:54
our And we also need to pass along the algorithm that we're going to be using. Now, our algorithm is going to be HS256, just like that. And in order to make sure this is consistent across everything where we sign our JSON web token. So, in this off section, I also want to pass along the algorithm. So, we can pass that along as the third parameter. That way we know we have the
- 1:07:14
same algorithm being used when we sign our keys as well as when we decrypt what our keys does. And this middleware just automatically decrypts what that particular value for our JSON web token is going to be. And we can have access to that JSON web token value. Now, in order to make sure we get all of our type safety set up properly, we're going to create a type here and this one's going to be called JWT ENV. That's for
- 1:07:35
our environment. And this is what we pass along to Hono. We can pass along an environment and this environment contains things like variables. So, we can say that we have a custom variable and this variable is going to be called JWT payload and we can give it whatever type we want. In our particular case, if I make sure I spell this properly, our JWT payload contains that sub that is our user ID. It contains our email,
- 1:07:56
which is a string, and it contains the expiration, which is going to be a number. This just matches up with all the stuff we passed into here. You can see this is a one-to-one match. So, we're just making sure we match that up. And if we pass that along as our type to our Hono JWT, now we have access to that in this variable section. So, we can see c.var, that's how we access our variables, and you can now see we have our JSON web token payload, which
- 1:08:17
contains all three of those different properties. So, if I wanted, I could, for example, destructure out just the sub portion and rename that to user ID cuz we know that is our user ID. And now what I can do is I can get all the keys for my user by just saying db.query. I want to query that API table. I want to find all of them where my user ID is
- 1:08:38
equal to that user ID. And I specifically only want to return certain columns. So, we'll come in here with columns and say the only columns I want to return is going to be the ID column, the name column, the key prefix column, and the created at column. Essentially, I don't want to return the hashed version. So, I don't want to return that key hash because that would be insecure
- 1:08:59
to be able to send that to the user and not necessarily needed. Then I can return my JSON for all my keys, just like that. And now that is hooked up the ability to get my API keys from our database. Now currently, we don't have any API keys stored in our database. Let's make sure that this JSON web token stuff is working like we expect it to by hooking this up with Requestly. So what we need to do is we
- 1:09:19
need to create a brand new collection. We'll come in here. We're going to call this one API keys. Give that a quick save. And inside of here, let's make sure that we have a request. This request is going to be our list request. So we'll call that list. And we know that it's going to take in our URL for our base URL, API keys, just like that cuz we're just trying to list out those API keys. Now if we just
- 1:09:41
try to call this as is, we should get some type of error or warning. If we send this along, you can see automatically we get unauthorized sent to us because we don't have any JSON web tokens being sent along. We need to send those along inside of our headers. Now I could specify my headers right here for my authorization and specifically this authorization section right here. But instead, I want to inherit this from the parent cuz every API request is going to
- 1:10:02
use the same off. So here, I'm going to specify my authorization and we're going to be using a bearer token. That's what's expected using JWTs. And here, I'm going to use a variable for this. We're going to set that variable to JSON web token, JWT. And we can create that right here. So let's create a variable called JWT, just like that. So now I have this JWT variable. If I give that a
- 1:10:23
quick save, that is being used and I can set that variable or do whatever else I want with it. So for example, when I try to log in as a user, and let's make sure that I register as a user first, and then we'll log in as a user. You can see I'm going to get this entire token. I'm going to copy this over. I'm going to go into my API section. I'm going to paste that down as my current value. Give that a save. And now when I try to list this out,
- 1:10:44
everything should work. I click send. And right now we're getting an internal server error. And I already know the reason for that. It's because we never migrated our database. So let's go ahead and make sure we do that. So we'll come in here. We'll generate all of our migration data. And then we'll make sure that we run a migration. That should send everything over. And now hopefully when I do a quick send here and I look at this, you can see I get an empty array being
- 1:11:04
returned. So it is properly getting our data, sending it along, and getting that information from our headers because of our API key right here. Now to make this a little bit more automated than what we're currently doing, I'm going to do a same thing with my post responses here. I'm going to do this with our login. So for our login post response, I want to store an environment variable. So we'll say environment. This is going to be my JSON web token. I want to get
- 1:11:25
specifically the token response from here. So I'm going to be creating a variable called JWT in my environment that's going to be set to whatever this token is. So now what I can do, instead of storing my variable here, we're just going to completely delete this. We don't need it anymore. What I'm going to do is I'm going to create an environment variable here called JWT. There we go. Give that a quick save. And now that's where my variable is going to
- 1:11:46
be stored, and everything is going to be automated for me. So if we look inside of here, authorization, you can see that's being pulled from the environment properly. And if I were to log in as a user, it's going to save that token inside that environment variable. So now if I try to send this along, I still get that empty array. If I hover over this, you can see it gives me that current value, which is that value that's coming from that environment variable. Again,
- 1:12:06
we're using Requestly to automate all of this process for us. Now let me show you how we would actually go ahead and create our own API keys cuz right now we're getting API keys, but we have no way to create them. So let's do an app.post. We want to just post this main route. We want to do an SValidator here. This is going to be for JSON. And we specifically want to come in here
- 1:12:27
with our create key schema. That's going to be a schema that we create. And then we're going to get our asynchronous function right afterwards. So let's create that right now. We can say const create key schema. That's a Z object. Make sure I import Zod. And the only thing this needs to have is a name.
- 1:12:47
So we'll come in here with a string. And we'll give it a minimum of one. Let's say it has a max of 255. So, we're able to name what each of our API keys is. That way, when we get that data, we know what the name associated with the API keys is, as well. So, now we can do inside of our function is we can use the exact same code up here to get our user ID, so we know what user we're trying to
- 1:13:08
create for. We can also get our body. Specifically, I care about the name portion. So, we can say await c.request and we want to get the valid portion of our request for JSON, just like that. That's going to give us this name property that we want to use. And then I want to generate an API key. So, we'll say API key is equal to generate
- 1:13:31
API key. And this is a function that we're going to create, which will just generate a generic API key. We'll do it in this function right here for our crypto. So, export async function generate API key. Now, this is going to be a very basic function. First of all, we're going to get a raw key. That's just going to be some random bytes. We'll say it's going to be 32 random bytes. And we're going to convert that
- 1:13:51
to a base 64 string. So, all we're doing is we're just getting a bunch of random data that's going to be returned to the user. Then we're going to hash that data. So, we can say hash API key. And actually, instead of creating a custom function for this, we're just going to write this inline. We're going to call create hash coming from node crypto. We're going to pass it in our hashing function. We're going to use SHA-256 for this.
- 1:14:12
We're then going to call update on this by passing it in the raw. And then we finally say a digest of hex. Essentially, what that's doing is creating a hashing function using the algorithm of SHA-256. We're then going to be hashing this raw value. That's what update raw does. And then we're converting that to a hexadecimal value. The reason I'm not using this fancy scrypt function is cuz we're not doing any salting or anything like that. We're
- 1:14:32
just hashing this, so it's slightly more secure when we go ahead and try to use it. Next, we want to get the prefix from this, which is just going to be our raw value.slice. And we're going to slice off the first eight things from it. So, essentially, this gives me the first eight values of my raw strength. Finally, I just want to return all this data as is. So, that way I can store this in my
- 1:14:53
database. So, now if we go over into our API keys, we have generate API key. And this gives me inside of here if I make sure I await it. There we go. It gives me my hacks, my prefix, and it gives me my raw data. And this function actually doesn't even need to be asynchronous, so we can completely remove all the async code from here cuz nothing inside of here is asynchronous. This hashing is entirely synchronously
- 1:15:14
done. Now, back to that API key section. We have essentially everything we need to do to insert this into our database. So, we can say db.insert. We want to insert in our API key table. We want to set some values. We want to set our name. We want to set the user ID. We want to set the key hash, which is just set to our hash. We want to set our key prefix, which is our prefix,
- 1:15:36
just like that. And that creates a brand new API key for us. So, we'll say const API key is equal to awaiting that. And since this returns an array, let's make sure we do that. And the only value I care about returning from here is going to be the ID. So, we can just come in here and say ID is going to be API key table. id. Cuz I just want to get the ID of that API key so we can use it in
- 1:15:56
other places. Now, I can return down my JSON. So, we'll say c.json. I want to return what my raw key is as well as the ID for that API key. There we go. And we'll set along a status 201 saying we created this. The reason that I'm setting everything up like this is because now when they create a key for the very first time when they create it, they can see what the full API key is.
- 1:16:18
And now every other time they try to access their API key, they can never see the full version. This is a security measure. They only see it one time. They can use it in their application, and if they ever maybe lose that API key or forget what it is or it gets hacked in some way, they can come here, delete it, and create a brand new API key and replace that inside their application. This is the most secure way to handle
- 1:16:38
API keys pretty much anywhere that you're going to work with them. So, now let's set this up inside of our quest list so we can work on creating API keys as well. So, let's just come in here. I might as well just duplicate this one. This one right here is going to be called create. It's going to be a post method just like that. It's going to be going to that API keys endpoint. It has the exact same authorization inherited from the parent like we did with the other one. The only
- 1:16:59
difference is we need to pass along some JSON in the body. So, let's come in here with some JSON and this is going to be the name field. And inside of here, let's just do like a random company name or something like that. Sure. There we go. It doesn't really matter. It's just going to be the name of our API key. So, now if we give that a quick save and I click send on this, you can see currently we're getting unauthorized being sent along.
- 1:17:19
Most likely because our JSON web token has expired. So, at least our expiration is working properly. Let's try logging in properly. That gave us a new one and now if we send along create, you can see it's giving us a key. There's the full value as well as an ID for that. And if we try to access our list of these values and I send this along, you'll notice here I'm getting that name and I'm only getting the first eight values
- 1:17:39
from my key. It never returns to me my key again. The only time I can access it is the very first time I create it. You can see there's the full key. Otherwise, every other time it just gives me a partial version of the key. Now, just like we did with all of our other things, I want to set up a post response script for this. So, let's just go to register cuz that is essentially what we want to copy. So, I'm going to copy that over into this section right here. I
- 1:17:59
want to set a collection variable for whatever my last ID is and set it equal to that ID. And I also want to do the exact same thing with environment variable. That's what I did for my login section. I created an environment variable. So, we're going to create an environment variable here that's going to store what our key is. So, we're going to call this API key and we're going to set that to whatever our most recent key is. So, now I have access to
- 1:18:20
both my most recent ID and my most recent API key. And if I go into my environment for dev, I can make sure I set API key right here as a value. There we go. So, now I can actually use that anywhere else I want. So, now when I create an API key, send this along, it should be storing all that information. Now, right now it says it could not find the collection variable. Let's make sure we set that.
- 1:18:41
Last ID, give that a quick save. There we go. Looks like that worked. Inside of this section, let's make sure we save that as well. And now, if we send this along, we should see that everything is working properly and that value is being saved as our current value. That way, when we set up our delete, which is what we're going to do next, we can make sure we hook it up the same way we did delete for our authors and so on with this nice little last ID trick. So, now we have our API keys. We can list all the API
- 1:19:03
keys for our user. You can see they have a bunch of them right now. Let's go ahead and work on being able to delete those API keys as well. And this is going to be relatively simple. We can come in here with a delete for a specific ID. We can make sure here we get our ID, which is just request. Whoops. C .request .param and we want to get the ID
- 1:19:23
specifically, just like that. There we go. And now, we can just do a simple delete. So, we can say delete from the table and we can just do it a where clause. And we want to do it where equals API key table.id equal to that ID that we pass in. We also want to make sure that the user ID is equal as well. So, we'll throw an and
- 1:19:44
into here. And we'll set an equal on there that the API key table.user ID is equal to our user ID. There we go. Make sure I get my imports all done. Don't need to return anything. And that right there is going to be to our delete entirely done. We can just return a null body
- 1:20:06
with a 204 saying that we deleted this. So, let's go ahead and actually test this out to make sure that it works. We'll come in here. We'll create a brand new API key. We'll just give that a quick send. Now, we have that key right here. That last ID is already being stored. We can create a brand new request. I might as well just duplicate this one right here. This one is just going to be called delete. We'll send that delete status along. And
- 1:20:27
again, inside of our parameters here, we want to delete a specific ID. And we can just set that value to our last ID, just like that. And everything else should be working automatically for us. So, let's just try to send that along, and you can see nothing happened, which is actually good. There's no response. Give that a quick save. So, now let's just do a double-check to make sure everything works. Let's create an API key. We're
- 1:20:47
going to click send. You can see this one starts with SRSIV8. Whatever, it doesn't matter. When we go into our list, and we look at the very bottom or somewhere, we should see that one. Yeah, there we go. SRSIV8. That's the one right there. If we click delete, it should delete that most recent one. So, if we click send, you can see that that one is no longer in our list. So, our delete is working like we expected to. And now we have access
- 1:21:08
to an API key, which we can use to restrict certain things. Like, I don't want to let anyone create an author. I only want to let logged-in users create authors. That way I can make sure to associate them with it, and so on. So, to do this, I actually want to set up authorization that works on our entire section here. And what we can do is we can set this to API key. We can set whatever key we want. I'm going to just going to use the default of X-API-Key.
- 1:21:29
And then we can use a value here. I'm going to use that variable we created called API key. And this is going to be added to the header of every single request. So, if we give that a quick save, now every single request that gets sent along that inherits from this is going to require an API key. For example, our off section, we don't want to inherit from this. So, we're just going to say that it has no off. So, we're going to clear that out. But our
- 1:21:50
author section, you can see in authorization, it inherits from the parent, which means it's going to automatically send along that API key. So, every request we send to our author section, for example, create, is going to send along that API key. Now, right now we don't have anything being sent along. Let me just try to register as a user. Let me try to log in as a user. And when I do that, I'm still getting an error. I think that's because this authorization is also inheriting from the parent. Let
- 1:22:11
me just clear that out. That way there's no authorization for our login and register. And now when I send, you can see it is properly trying to send that information. So, when we try to log in, it's going to log me in properly, gives me that token, and now I can use that token to create an API key, which now that API key is being set inside of everything. So, now if I were to go ahead and try to like access my author, I'm able to access it cuz an API key is
- 1:22:31
properly being sent along. So, now let's go modify our code so we can access that API key. Inside this section that we have for our auth, so we have our auth right here. This is where all of our authentication code is going. I want to create a brand new section for our middleware. So, up here inside of our source, let's actually just create a brand new folder called middleware. And in this middleware folder, I'm going to create one called auth
- 1:22:53
.ts. And this is where we're going to create a function API key auth. And I'm actually going to create that as a constant variable and set that equal to the create middleware function. There we go. That comes from the Hono factory. And this create middleware is going to take in a generic type. So, we're going to create a type for that.
- 1:23:13
That's our API key ENV. Kind of the same exact thing we did for our JSON web tokens, but this is for our API key. So, that's going to take that in here. We'll set that up in just a second. And then this is going to be an asynchronous function that takes in our current context, as well as a function called next. And this next function just allows us to say, should we proceed or should we
- 1:23:34
immediately exit out? Now, for our API key, we want to set variables just like we did with JSON web tokens. And we specifically want to set one called our API key user. And this is just going to be equal to our user type. But, we're going to specifically pick only certain fields from it. So, we're going to say type of user table.infer select. And I want to pick just a few
- 1:23:56
fields. I want to just get the ID. I want to get the role. And I want to get the email. So, those are the only fields that I want to get for my user. So, we're going to be creating a new variable and sending it on our context called API key user. Now, to do that, we first need to get the API key itself. So, we can say const key is equal to c.request
- 1:24:17
.and we can come in here with our headers. And we created that header. We gave it the name of X-API-Key. And me make sure I spell that properly. There we go. And now we can just say, "Hey, if we don't have a key, obviously you have a problem." So, if our key is equal to null or our key.trim is equal to an empty string, that means we don't have a key. So, we just want to return some type of error saying that
- 1:24:38
they aren't authorized to access this. So, we'll say error missing API key. Just like that. We'll send along a 401 saying, "Hey, you didn't authenticate properly. You're supposed to have an API key to access this part of our API." Next, we can get the key hash, which is just equal to hash API key. And this is actually a function that we should create. So, inside of our crypto
- 1:24:58
section, where I hardcoded that function, let's just call this hash API key, which takes in our key. And then let's export that as an async function hash API key. Takes in a key. And then I'm going to just run that code by taking in whatever our key value is
- 1:25:19
and returning that to us. That way we can use that in other places, such as here. So, now we can hash that API key by taking the key from that, and we can now compare these hashed keys just like we did when we did the password comparison. So, let's get our API key just like that. We'll say await DB query. I want to query that API key table. I want to get it where
- 1:25:40
my API key, I want the hashed version specifically. Let me just make sure I have this proper. Actually, this is the API Oops. db.query .api key table.find first where and we want to get that key hash is equal to this key hash right here. Now, I accidentally set this as an async
- 1:26:00
function, so let me remove that async from there. That gets rid of our errors that we have. And now we can just check, "Hey, if we have an API key, that is at least one step further." So, if our API key is equal to null then we want to throw a type of error again saying invalid API key cuz they sent along an invalid API key that doesn't exist in our database. And finally, once we have an API key, we can
- 1:26:22
figure out what user that API key is associated with. So, we can say await db.query. We want to query the user table. We want to find the first one where the ID of the user is equal to our API key .user ID, just like that. And I only want to get a few columns from this. And these columns are just going to be the columns up here, ID, role, and
- 1:26:43
email. So, I'll say ID, role, and email, just like that. So, now we have our user object, and we can now use that. But, of course, first we want to check to make sure the user exists. So, if our user doesn't exist, again, we'll say they have an invalid API key, just like that. And then finally, at the very bottom, once we know they have a valid API key and it's associated with a user, we can say c.set. This allows us to set
- 1:27:05
a variable, for example, our AP key API key user. We can set it to our user like that. And then we can just call next, make sure that we await it properly, and that's going to make sure that it calls the next thing in line, which is going to be the function that uses all of our API key stuff. So, this right here is just a middleware. And the nice thing about middleware is we can use this anywhere we want. So, inside of this
- 1:27:26
author section, we have our routes here that anyone can access, but I want to make sure that creating, updating, and deleting are going to be protected routes that use middleware. Well, what I could do is I could paste the middleware into each one of these like this, and I could make sure it works like that. But, this is kind of cumbersome. So, instead, what I want to do is I want to create essentially a set of protected routes that all share that one piece of middleware. This will also make sure I
- 1:27:47
get full type safety as well. So, what I can do inside of here is I can create a protected app, which is a new Hono app. Make sure I give it that API key environment type. Looks like I probably didn't export that, so let me make sure I do that. So, inside that middleware, let's export this type. Then we can use it everywhere we want. So, here,
- 1:28:08
make sure we import that. And now, with our protected app, I can just call that API key by saying dot use just like this and now it's using that API key off middleware that we have and it's going to use it everywhere we use protected app. So here for the post, here for the put and here for the delete, we're going to make sure all of these are protected and are going to have access to that API key user if
- 1:28:30
we want it. So for example inside of each one of these, I could say I could get the user that is currently being authenticated inside this section. Now I don't need that user information but I already have the validation set up in the middleware so let's test to make sure that this works like we expect it to. Right now, I should be able to access all of these fields and if I look, you can see I'm able to get, I'm able to list, I'm able
- 1:28:52
to do a create. Right now we're getting a 404 not found which that doesn't make any sense. This is most likely because somewhere inside of our database queries is having a problem. So inside of our middleware, somewhere in here we're throwing some type of 404 is my guess. Let's just look at our console logs to see if anything inside of here is telling me anything. It is not. Let's just put in a a quick console log. We'll say console.log key
- 1:29:13
to see if the API key is being passed along. So now what I can do is I can click my send, still showing me that 404. If I look in here, it's not logging out anything at all. Okay, I know what the issue is. We created this protected app but we're not actually exporting it. We're only exporting our normal app. So what we need to do is we can say app.route at the base route, we can add in our protected routes.
- 1:29:33
There we go. So that's just going to add in all of our protected app routes to our normal app so that way they're all available everywhere. So now we should be able to access this wherever we want. So let's bring over that request and let's just quickly send this along and it does look like now it's properly sending along the information to create and we should be able to do a delete as well and we can do an update which will fail cuz the author is not there cuz we just deleted them. So let's try another
- 1:29:54
update just like that. Now what I'm going to do is I'm going to change my API key to make it incorrect. So I'm going to go to my dev environment where my API key is. I'm just going to type a bunch of random data in there. Now it's an incorrect API key. Now if I go ahead and I try to create a user, I'm going to get an invalid API key. But if I try to get a user or a list of user, this is going to work fine. You can see list works fine and get also should work
- 1:30:15
fine. Right now we're getting an error because I believe I don't have something created, but list is working fine because I do have everything being listed. So that is working like we expect. These are public routes while these routes are private and you need a valid API key to access them. If I were to create a new API key or I fix my API key that was there before, so let's just log in my user. There we go. And let's create an API key
- 1:30:36
and now let's try to do a create and now you can see that that's working. You can see here that my Let's do it one more time. There we go. And I can do things like list and everything is working and all those other things are just like I expect. Now the next thing I want to work on is a table for storing all of my books. Just to show you that now once we have this groundwork in place, it's really easy to add more stuff to our application.
- 1:30:56
So let's create a new schema for books. I'm just going to paste this in because it's relatively straightforward stuff. We have a title which is a string. We have a description which is a string, a publish date which is a timestamp, page count which is a number, our author ID is referencing the author table, and we're also referencing which user created this particular page as well. So now we know which user added this book
- 1:31:16
to our database and then of course our created at just like all of our other tables. Inside of our schema, let's make sure that we have our books inside of here. And our relations, we need to add our relations for our books and our authors. Now the author one is quite easy. You can see here for our author table, they have many books. So we're just going to have a many relation just like that. While our book table is going to be quite a bit more complex because our books are going to first of all have
- 1:31:38
a one relationship with an author and they're having a relationship with that added by user. So I just have those in there as well. It's the same thing we did for our API keys and our users. And then finally, our users have a books added. So we're going to say r.many and they're going to have many book table values. There we go. So that's just hooking up all of our different relations between
- 1:31:58
these different things to be able to be used inside of our queries that we want to do. Now before I forget, let's make sure that we run this in our database. So, let's come in here. We're generating our different migration files, and then we're going to migrate all of this into our database. That way, before I forget, we have all of our database tables in place. Next, I'm going to copy my author route and call this one book because this is going to be very similar between the two. And I want to come in
- 1:32:20
here with the books. And we're going to have our book routes. And let's just make sure I copy this down. Book routes and book. There we go. So, that's setting up our different API routes. And now, let's go inside of here and work on what we want. First of all, we'll need a create book schema, and we're going to need an update book schema. Now, for these schemas, it's same old stuff we've typed before. So,
- 1:32:40
we have a title, which is a string that has a minimum value of one, an optional description, an optional publish date, an optional page number, and then finally, the ID of the author associated with it. And for updating a book, it's pretty much a bunch of the same exact stuff, but we've essentially made sure that they're nullable and optional for all the different values, unlike create, which is required for everything. So, now, for our get function, we want to get the books, so we can come in here
- 1:33:02
with a query on our book table to get all of our books. And I also want to get the authors, so we're going to say with author and set that to true. And that's going to give me all the information for the author as well. And now, for my get function down here for an individual one, essentially, it's the same thing, but I want to do this with my author as well. So, we're going to say with author of true, and make sure this is my book
- 1:33:23
table. And we'll say book not found if we can't find the particular book. And we'll call this book. Just like that. So, now, let's go into the more complicated ones, which are going to be our create. So, let's get our create book schema in here. There we go. And we want to get all of our data from there, so you can see that's fully type safe. And we also want to get our user that was creating this. So, we're going to say const
- 1:33:45
ID, we'll rename that to user ID, is equal to c.get. And we want to get that API key user. That is going to give us that user ID information. And we could also, if we want to get the email and role, but we only care about the ID for this particular use case. Now, the next thing that I want to do is I want to make sure the author they're trying to add for the author of the book actually exist. So, we can say const author equals await db.query
- 1:34:09
the author table and I want to find the first one where the ID is equal to my data.authorID just like that. And if for some reason I don't have an author, so if my author equals null, then I want to return some type of error. So, we'll say c.json
- 1:34:29
error author not found. And let's just send along a generic 400 error letting them know there is some type of issue trying to find that author. So, now we have the author, know that it's valid. Let's go ahead and create the book. So, we'll say db.insert. Make sure this is asynchronous. We want to insert into that book table
- 1:34:49
just like that. We want to set the values specifically. And what we can do is we can take all of our data, pass that along as is. And the only thing we need to do is pass along the added by ID, which is the user that is creating this. So, we'll say added by is set to our user ID just like that. Now, here what we can do is we can just return the full object and that gives us our book, which we can return right
- 1:35:10
here. Give that a quick save and that should be everything for this create section entirely done. I want to essentially copy this because our update is going to be very similar. So, we'll come down here. Let's just say update book schema just like that. We want to get our role because I'm going to limit what can update what that's being supplied. And then here, I want to first again check to make sure our author exists. So,
- 1:35:31
we'll say if our data.authorID is not equal to null, that means we actually have an author ID, we want to check it. So, we'll get our author and make sure that our author is not equal to null. If it is, we'll throw an error. Next, I want to make it so that admins or can update any user book that they want, but users can only update books that they've created. So, we'll create a simple where clause here.
- 1:35:51
I'm going to say that if the role is equal to admin, then they can update whatever they want. So, they can update anytime the book is equal to whatever the current ID is for the book. So, let's make sure we get the ID. That is going to be coming from our C.request.params of ID. That's that ID property up here. So, where the book table.id is equal to our ID, they can
- 1:36:13
update that just fine. If it is a user though, they can only update things where the book ID is correct. So, we'll make sure that's in there. But, also where their ID is the same as the added user ID. So, let's make sure we come in and do that as well. So, we're going to say equal book table.user, I'm sorry, added by ID is equal to the user ID. There we go.
- 1:36:34
So, this is just making sure that if it's an admin, they can update anything. But, if it's not an admin, they can only update the book if they also created that book themselves. And now here, we just want to do an update. So, we're going to say update our where clause is just going to be that where clause we created. And then we want to specifically set values for all of our data. And we don't need to worry about this added by section, so we can just pass along our data as is just like
- 1:36:55
that. Now, the only reason I'm getting an error here is cuz I just need to move this down right there. That's going to be updating our book. And if our book is equal to null, then we just want to return some JSON that says book not found. Give that a 404 error just like that. And then we can return our book right
- 1:37:16
here. Don't need to worry about passing a status cuz it'll use the default of 200 status. Finally, we're moving on to delete, which is going to be really straightforward. We're just coming here with our book table or our book table ID is equal to that. And that should be everything right there for delete. But, actually I forgot. We want to make sure again we use the same restriction we did up here for our where clause. So, I kind of want to actually copy everything I have here,
- 1:37:37
bring it down into our application. Let's just put it right below our ID here. I want to make sure we don't need to worry about checking the author, but we do need to get our where clause properly. So, here I want to add this where clause directly into there. That way, only users that have access to all of the books can delete it. Or, if it's your own book, then you can delete it. So, now let's go set this up inside of
- 1:37:57
our Questly. So, what we can do is we can create a brand new collection. This is going to be for our books, just like that. And inside this books collection, when we give that a save, we can add in all the different requests we want. So, let's do a request for a list. And we know that our list is going to be pretty similar to what we did for authors, so I might as well just copy that over. Go to where we had that one opened.
- 1:38:18
We'll just create a brand new one, I guess. Call this list. There we go. Paste that in there. This is going to say books, just like that. Give that a quick save. And if we send, you can see we're getting an empty array, which is exactly what I would expect. Next, I want to do a response. This is going to be for a get of a single value. So, let's just make sure we do that. Give that a quick save.
- 1:38:38
We'll copy over what we are doing for our list. Paste this in here with a specific ID. And we'll use this last ID value, just like we've done with everything else. So, here last ID, just to make sure all that works properly. And let's make sure that we save all these values. So, now inside of our get, if I were to send this, we're going to get a 404 cuz we don't have any last ID
- 1:39:00
yet, which is kind of what we would expect. Now, let's go ahead and come in here with create as well. So, we'll create a brand new request. Call that one create. This one is going to be a post. Give that a quick save. Copy over the URL. Just like that. And we need to pass along all of our different JSON body information that we need. Now, we actually have quite a few values here.
- 1:39:20
For example, title is the very first one. And we can just do like some random string or something along those lines, or maybe it's going to be like random lorem. There we go. Let's just do a random lorem word. That'll work perfectly fine. We're going to come in here with a description.
- 1:39:40
There we go. This one is going to be random lorem words. Sure, doesn't really matter. We'll come in here with a publish date. Let's just do random date past. Perfect. Page count.
- 1:40:01
Random integer. And then finally, we need our author ID. And for that, we can just essentially hardcode a value. So, let's just get an author. We'll just list out our authors. And actually, we need to make sure we have our API key. So, let's make sure that we just register an account. We'll do a quick login. We'll create an API key, and then we'll come over here
- 1:40:22
and make sure that we just create a brand new author, and we'll just use their hardcoded ID. That's going to be perfectly fine for our use case. There we go. And our integer is a number, so let's make sure we don't wrap that inside of quotes. That should be everything we need to do to do a create. So, if we send this along, you can see it's actually creating that, and it's hooking it up with my current user ID that I am logged into, which is exactly what we want to make sure happens. Now,
- 1:40:42
let's do update next. So, what I'm going to do is I'm just going to duplicate this one. We're going to call this one update. So, we'll make that a put. Update, just like that. Give that a quick save, and make sure inside the body everything is set up. And for our title, we'll just put new in front of it, so we know that this is the newly updated version. So, now if we just give that a quick save and send, we're
- 1:41:03
getting a 404, and that's because I haven't set up my post scripts yet. So, let's go in here for my post response script, set that up on my create. There we go. Last ID is coming from my response. Make sure this is a post request script. Oops. Let's just copy that over again. There we go. And go into the post
- 1:41:24
request and send that along. There we go. That should get our ID set on our book value right inside of here. And then when we do our update properly, we should be able to say ID and set that equal to our last ID. So, now if I just do another create here and now I do an update, hopefully you can see it's working. It does look like it's giving us our data and a success of false
- 1:41:44
invalid input expected date received a string. That's probably because of our published data or something like that. Let's just not update that, so we'll just remove that so we don't even need to worry about that particular section. Let's try sending that along and now that does work. Now let's actually look at our code to see why that did cause a problem with our date. I think it's just cuz I'm not coercing my date, so let's scroll all the way up here. You can see this published date.
- 1:42:05
This should have a coerce on it and same thing with my page count here. Actually, that's fine. But just the date does need to be coerced. So, that was why I was running into issues there, so that solved that problem. Let's go ahead and finish this up by adding in a delete section. So, let's come in here. We'll create a brand new HTTP request. Delete. Make sure that we save this. We'll copy over this URL. Set this to
- 1:42:27
delete. Paste down that URL. And we just want to make sure we have our ID. And our last ID. Get that a save. And now if I send that, we should have deleted that, which is exactly what we wanted. So, now let's make sure all of our admin related stuff works like we expect it to. Let me create a brand new book. So, I've created a book. The title is TOT. I have an ID associated with
- 1:42:47
that. I'm just going to copy over that ID. I'm going to manually hard code my ID here as that value that I just created. Now that I've saved that value, I want to register as a completely different user. I want to log in as that user. I want to create an API key for that user. So, now I'm logged in as that completely different user. And what I want to do is I want to see, can I update that book that was associated
- 1:43:07
with a different user? When I click send and we look at this, you can see we get book not found. That's exactly what I would expect to get cuz we want to return that we couldn't find something if the user does not have access to that particular book. So, this is working like we expect, but if this user was an admin user, they would have access to this information. We can relatively easily test that by coming into here and we can run NPX Drizzle
- 1:43:31
Kit Studio. That's going to allow us to essentially modify the data inside of our database directly. We can come into here. I can find that section for wherever we have our users, and I want to get whatever my most recently created user is. So, let's just filter by that particular row. You can see that all the way down here is our most recently created user. I'm going to set the role here to admin. Give that a quick save,
- 1:43:53
and hopefully just by doing that, that is going to actually make it so we can work with this data. So, let's open up Requestly here. I'm going to click send. And you can see we're able to update that book because we're now marked as an admin user. So, all of our data is hooked up properly. Now again, if you enjoyed this Requestly tool, I think it's absolutely amazing and a great replacement for Postman since it's so much lighter weight and doesn't have nearly all the bloat. It's going to be
- 1:44:13
linked down in the description below. It's entirely free for you to use. And I want to send a huge thank you for them for sponsoring this video. With that said, thank you very much for watching and have a good day.
Description
SponsorTry Requestly For FREE: https://requestly.com
Hono is an amazing library that makes building type safe backend APIs incredibly easy. In this video I will show you how to build a fully type safe API from scratch that utilizes email/password auth, JWT, and API keys to secure the application. I will also show you how to use Requestly to make testing your API so much easier.
📚 Materials/References:
GitHub Code: https://github.com/WebDevSimplified/h...
🌎 Find Me Here:
My Blog: https://blog.webdevsimplified.com/
My Courses: https://courses.webdevsimplified.com/
Patreon: / webdevsimplified
Twitter: / devsimplified
Discord: / discord
GitHub: https://github.com/WebDevSimplified
CodePen: https://codepen.io/WebDevSimplified
⏱️ Timestamps:
00:00:00 - Introduction
00:01:15 - Hono Setup
00:02:37 - Basic Routing
00:08:32 - Requestly Setup
00:10:24 - Author CRUD Implementation
00:20:00 - Automating Requestly
00:25:58 - Database Setup
00:41:25 - Auth/JWT Setup
01:02:53 - API Key Setup
01:30:55 - Book CRUD Implementation
#Hono #WDS #Requestly
More from Web Dev Simplified
×2244:57The Best Local Agentic Coding Workflow (Complete Guide)
Web Dev Simplified1.1M views
×9.72:10Please… I Need Your Help
Web Dev Simplified459K views
×6.91:15:25The Ultimate MCP Crash Course - Build From Scratch
Web Dev Simplified327K views
×4.925:15I Will Never Use Prettier or ESLint Again
Web Dev Simplified231K views
