Build a REST API with Node.js, Express, TypeScript, MongoDB & Zod

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
if you're a junior to mid-level developer and you want to learn how to make rest apis like a senior developer stick around because in this video i'm going to show you how to make rest apis that will make any cto weak at the knees so who is this video for it's for junior to mid-level developers and anyone interested in building rest apis with typescript what will you need you'll need a running instance of mongodb you'll need postman you'll need an ide or text editor i'm going to use vs code you'll need a web browser and you'll need a package manager such as npm or yarn and you'll need nodejs installed this video is going to be the first in the series so let me know what you want me to make next i could do a video on testing the api with jest building a react user interface adding prometheus metrics to the api deploying the api with cadi and docker or adding google oauth the concepts that we're going to cover are rest api principles such as crud and http methods we're going to cover jwt and refresh tokens and we're going to cover request validation to build the api we're going to use node.js mongodb with mongoose typescript express and express middleware and we're going to use zod for validation this is the video structure that we're going to follow if you have a look in the description below you'll see some timestamps to each section of this video in the readme there's some of these data flow diagrams which i'll refer to throughout the video to add a little bit more context to what we're building so you'll be able to find the link to this repository on github and there's a few things from the repository that you'll need to get started so the first thing you'll need is this postman collection.json and i'll show you how to load this in postman in just a minute and you'll also need this notes.md and this is going to have your yarn add commands so this will be yarn add to the dependencies and then we have some development dependencies down here as well and that'll save you from typing these out so let's copy the contents of this postman collection.json and we'll head over to postmen to import the collection so i'm in postman and i have an empty workspace and i'm going to go up to file i'm going to select import then i'm going to select raw text and i'm going to paste the json into the raw text bar and i'm going to click continue if we expand this folder that says rest api tutorial you'll see we have two folders inside the first one is user and then the second one is product if we have a look at create user you can see that we have a body and the collection also includes some variables so we have an email a password and a name configured in the variables we have create session and this is going to be used to log the user in if you have a look at the body we're going to use the same email and password we also have a test here so once we run this and we get back an access token and a refresh token we're going to set these in environment variables the access token and refresh token will be used in subsequent requests so you can see here we have a bearer token and this is going to refer to our access token and in our headers we have this x refresh header and this is going to refer to our refresh token in these requests we also have another test and this test is going to say if there is another access token then reset our access token to that new access token and the reason we'll get new access tokens is because our old access token has expired and the api has used our refresh token to issue a new access token we have delete session and this is used for logging the user out we have create product and create product has a body defined and it has the access token defined in the header we also have a test for create product and this is going to say that once we get the product back set the product id in a variable called product id and we're going to use that product id for subsequent requests on the product so we can get that product that we just created and we can run this update and this update is just going to change the title description price and image and finally we can delete that product if we like let's run all these requests so i can create a user you can see we get our user object back i'm going to log in as that user and you can see we get an access token and a refresh token and if we go to get sessions we're going to request all the sessions that this user has open and we can see that our access token here is now set to ajwt and our refresh token is also set to a jwt so let's click get sessions and you can see that we have one object here and this is because we're flogged in once and so therefore we have one session we can log that user out with delete session but before we do that let's create a product you can see here our product id is product m652 we go to get product and our product id is set to product m652 so we can click get on that and we get our product object returned we can update the product and when we get our product we should see the price down here change to 6.99 and that product price has changed and finally we can delete the product so if we try to get this product again we should expect to see a 404 not found so before we get started bootstrapping the application i'll give you a walkthrough of the code so in our file explorer you'll see a config folder and a source directory these are the two most important folders so in config you'll see one file and this is going to be our default config and this is just an object and we're going to have some config in here like a port our host our database connection url our salt work factor that will be used in the user model access token time to live it's currently set to 15 minutes a refresh token time to live is currently set to one year and we have a public key and we have a private key these keys are just keys that i generated online so feel free to use these same keys if you like or you can go generate your own keys if you're going to deploy this application i recommend you use your own keys because these are going to be in a public github repository in our source folder we have an app.ts and this is going to be the starting point of our application we have our routes and this file is going to export a single function that is going to handle all of our express routes we have a controller folder and this is going to have a controller for each resource so the resources that we have are product user and session we have our middleware and we're going to have three bits of middleware one to deserialize the user one for requires user and one for validating requests we have our models and again we're going to have one for each resource so we're going to have one for product user and session we're going to have our schemas and these are going to be used by our validate request middleware and these are going to be used for validating inputs we have our services and services are called by controllers and again we have one for each of our resources and finally we have utils and utils is going to include a connection to our database our logger utility and our jwt utilities if we go back to the readme we can see the request structure so this diagram here tells us that a request is going to come in via a http endpoint it's going to go to some middleware if there's middleware in the route the request is going to go through our controller our controller is going to call our services our services are going to call the database and finally the response is going to travel back up through the database through the service and the controller and to the user making the http request so let's get started bootstrapping the application so i'm here in vs code and i'm going to open a new terminal by clicking terminal new terminal and i'm going to type yarn in it if you're using npm the command is npm in it and this is going to create a package.json for us and i like to just press enter all of these questions next we can open up our notes.md again this is going to be on the github repository i can paste this command in and this is going to install all of our dependencies for us once our dependencies are installed we can do the same with our development dependencies once our dependencies have finished installing let's create a new folder and i'm going to call this folder source or src and i'm going to create one new file in source and i'm going to call this app.ts inside of app.tiers i'm just going to say console.log hello world we're going to open up package.json and we should add a new object to package.json and i'm going to call this scripts and inside of scripts i'm going to make a dev command and my dev command is going to be ps node dev and ts node dev is a dev dependency that we installed earlier we have a look down in our development dependencies you should be able to see ts node dev here i'm going to give this a flag of respawn and i'm going to give it another flag of transpile only and then finally i'm going to point it at source app dot ts type yarn dev and we should see hello world in the console if we come over and change hello world to hello world from our api and save this file we should see that the file has been modified and our server has restarted and we get a new output so we're good to go let's start by importing express from express and i'm going to say const app equals express and i'm going to say app.listen i'm going to pass in a port i'm just going to hard code this port for now we're going to add this to config later on let me say 13337 and i'm going to add a console.log for now says app is running you can see here that we get this weird error and this is because we don't have a ts config file so let's fix that i'm going to say mpx psc and i'm going to give it an in-app flag you can see that we have successfully created a ts config file and we have a bunch of default options here while we're in our ts config file let's add an out directory and i'm just going to say that this is build and so when we build that application it's going to go into this directory here let's type beyond dev again and you can see we get app is running so let's set up config so we can get our port from configuration to create a new folder in the root directory and call this config inside of config create one file called default.ts and this is using the config module that we installed so if you look up on google config npm you should get the docs for this module i'm just going to say export defaults and we're going to export one object here the first property i'm going to export is a port and the port is going to be a number of one two three seven you can make this port whatever you like just note that it's set to one three three seven in the postman collection i'm gonna add a db uri and i'm going to set that to mongodb localhost 27017 and then i'm going to make the collection rest api tutorial you can obviously call this whatever you like and this is all the configuration i'm going to add for now to come back into app.ts and import config from config and say const port equals config.get and this is the name of the property that you want to get so inside a default this is going to be called port so we want to get our port and get takes a generic if you command click on get you can see here that it takes a generic type so we can set this type in here as a string and then we see port now has a type of string it's actually not a string it's a number and port has a type of number so if we hover over this end you can see that port in listen takes a number so this port here is going to fit perfectly into here let's create a new folder inside of source and we call this utils inside of utils i'm going to create one file and i'm going to call this connect.ts and this is going to house our database connection i'm going to import mongoose from mongoose i'm going to import config from config i'm going to say function connect and then i'm going to export the connect function as default and say constdb uri equals config dot get and in our default config we set our db uri so let's get that out of there again let's set this to a string let's return mongoose.connect we're going to pass in the db uri as the first parameter i'm using mongoose version 6 and if you install the dependency without specifying a version you'll be using that version as well so because we're using version 6 this use new url parser and use unified topology and not options that you need to set if you're using a version previous to version 6 you might need to set these properties but we're just going to pass in our connection string i'm going to call dot then i'm going to say console.log connected to db otherwise i'm going to call dot catch i'm going to catch the error and say console.error would not connect to db and i'm going to call process.exit and i'm going to give it a exit code of 1. this means it exited with an error if you want to you can use try catch here instead so i could say try and i can call mongoose.connect and i will await that this will therefore need to be an async function and i can catch the error and i can move the contents of the catch block up into this catch block and we've just converted this to async await let's come back to app.ts and we're going to import our connect function i'm going to add async to our listen callback function i'm going to say await connect to restart and we should expect to see our database connected logged in the console sorry i didn't add a console.log itself console.log db connected and we get our databases connected so we're using a few console.logs and this isn't great let's fix this and so inside of utils i'm going to create a logger dot ts file inside of logger i'm going to import logger from pino and i'm going to import day js from djs and pino is going to be the logger and djs is just so we can format the timestamp so let's create a new cons called log and i'm going to make this equal to logger and i'm going to execute the logger function and i'm going to pass in some properties i'm going to say pretty print it's true i'm going to say base and base is going to be an object i'm going to say pid is false and pid is the process id then i'm going to add timestamp and timestamp is going to take a function so i'm going to add a string here there's time i'm going to add a full column i'm going to call day js i'm going to execute that function and i'm going to call format and let's export default log let's come back into our app.ts and we can import our logger and we can remove our console.log with logger.info let's also update this app is running to app is running at and we can provide a full path test we'll say http colon slash local host and we'll put our port in the template string and you can see here we get a nicely formatted timestamp with a log level of info and we get our log message here let's go back to our database connection and i'm going to import logger from logger and i'm going to replace this console.log with logo.info and i'm going to replace console.error with logger.error in our source directory let's create a new file and we're going to call this file routes and routes is going to be responsible for taking the http request and forwarding it on to a controller they're going to say function routes i'm going to export default routes and routes is going to take one argument and this one argument is going to be app and this is going to be of type express and we can import our express interface from express let's go back to our app.ts and we can import routes from routes and below our database connection we can call routes and we need to pass in our app that we initialized up here so let's create our first route i'm going to make this a health check route so i'm going to say app.get slash health and this is just going to return a response status of 200 so i'm going to get our request object and our response object and let's import those interfaces so you can import request and response from express and i'm going to call res.send status and the status code is just going to be a200 so the theory is that if this endpoint returns to 200 our api is up and running so i'm going to open up a new terminal here and i'm going to call curl and i'm going to call htp localhostport13337 and i'm going to call slash health check and you can see here that we get an ok and that's the default message for a status code of 200. before we move on and create any more endpoints i'm just going to create one piece of middleware that we're going to use in a lot of endpoints so in our source directory i'm going to create one folder i'm going to call this middleware and inside of middleware i'm going to create a new file and i'll call this validate resource dot ts so what validate resource will do is when a request comes in we're going to provide a schema in the middleware and then it's going to validate the request against that schema so for example when we're creating a user you require an email and password and we're going to make sure that both of those fields are present and we're going to make sure they're both strings and along with that we're going to make sure that email is actually an email and the validation library that we're going to use is zod but you could use any other validation library such as joy or yup so i'm going to create one variable called validate and i'm going to make this equal to a function and then i'm going to make that function return another function inside our first function we're going to take this schema and inside the second function i'm going to have a request a response and a next function i'm going to import those types from express request response and next function let's type these up so this may look a little bit funny but this technique is called currying and the reason that we want to use currying is because we want to be able to execute this function here with our schema inside of middleware and then we want that to return another function and that next function is going to take the request and response in next and this is a express route call and then it's going to validate that request object against that schema so let's type our schema so i'm going to import from zod i'm going to import any zod object and that's going to be our schema type i'm going to create a try catch block and then i'm going to call schema dot pass and say body request dot body query request dot query rams request dot brands and this is going to allow us to create schemas where we can validate the body of the request query parameters and parameters and anything in params so if we find an error let's res let's return res.status 400 and i'm going to send e dot errors so if our schema doesn't validate then it's just going to return a 400 and it's going to tell the user where the errors are let's type our error here as any and i'm going to export default validate the next thing to do is to create a user model inside our source directory let's create one new folder and we're going to call this folder models and inside of models i'm going to create one file called user.model.3s the reason i put model in the name is so you get this nice indicator up top here because when we have controllers and schemas for user they're all just going to say user at the top here if you just call them user this way we can see that this is the user model so inside the user model let's import mongoose from mongoose import be crypt from b crypt and we're going to use bcrypt to hash our user's password import config from config let's say const user schema equals new mongoose dot schema and mongoose.schema is going to take one argument and that one argument is going to be an object so it's going to take two arguments the second argument is also an object it's going to be timestamps and we're going to set that to true and timestamps is going to give us a creator and updated date on our objects automatically let's create one field called email email is going to be of type string going to be a required field and it's going to be a unique field this means that you have to have an email and you cannot have two users with the same email i'm going to have a name going to be of type string and it's also going to be required finally i'm going to have a password this is going to be of type string and it's also going to be required i'm going to declare a constant called user and this is going to be the model so this is our schema definition and then this is our model this is going to be equal to mongoose.model a model is going to be called user and it's going to take our user schema let's export default user if you like you can call this user model and you can export user model let's create a typescript definition for this user schema so i'm going to say export interface user document extends mongoose dot document in the mongoose documentation they recommend that you don't extend mongoose document but i find it easier to create documents like this you might find it easier to not extend mongoose document it's really up to you there's lots of ways that you can integrate mongoose with typescript one of them being type goose but this is my preferred method you need to experiment and find your preferred method i'm going to match my schema so this is going to have a email of string i'm going to have a name of string i'm going to have a password of type string we're going to have a created app and this is going to be a type date and the reason that we're going to have created it is because we have this timestamp true and i'm going to have an updated at and that's going to be another date so when we create a user we want to take their password and we want to run it through a hashing algorithm and then we want to store the hashed password so if somebody puts in the password password123 we don't want to store that in plain text we want to store a hashed version of that so when we go to compare the password we'll hash that password again and then we'll compare that hash against the hash that is saved in the database and this means that if our database is compromised we don't link a bunch of passwords out into the internet so let's add a pre-save hook to our schema so i'm going to say user schema dot three and this is going to be called on pre-save and i'm going to create an async function and this async function is going to take one argument and this is going to be next and this is going to be type of mongoose dot hook next function i'm going to say const so i'm going to say let user equals this as user document so this is a little bit old school in some old code bases you would see let self equals this and this is a handy little trick so you don't have to reference this directly so we're going to add a little condition here that says if user dot is modified so this is going to be if using it's not modified and then we're going to pass in password so if the pre-save is not modifying the password it's going to return next if the password is being modified we're going to create some salt and then we're going to have a hash and then we're going to replace the user's password with that hash they say const salt equals a weight i'm going to call bcrypt dot gen salt and we need to create some salt work factors in here and we're going to add that to config let's come over to our config and i'm going to add a new property called salt work factor and i'm going to set this to 10. so this is how many rounds should you sort the password you can set this to whatever number you like 10 is a pretty standard number let's say config.get and then i'm going to get our salt work factor and we can type this as well as a number next i'm going to create a hash so i'm going to say const hash equals a weight bcrypt dot hash sync and i'm going to pass in the user's password and then i'm going to pass in our salt then we're going to replace the user's password with our hash i'm going to say user.password equals hash then i'm going to return next and as far as i am aware using this salt with bcrypt hash is the strongest password hashing algorithm that you're going to get if there is another one please let me know in the comment section below and i'll make a video on that next when the user logs in we want to be able to compare their password that they provided us with the hash so let's create a handy little utility to do that so i'm going to say user schema dot methods dot compare password is equal to an async function and they're going to supply a candidate password that is going to be of type string so when they fill in the little box on the user interface with their password this is going to end up in this function here as their candidate password and say const user equals this as user document and i'm just going to return decrypt dot compare and we're going to compare the candidate password with user.password then i'm going to catch i'm going to get the error and we're just going to return false so what this is going to do if the candidate password is correct it's going to return true otherwise it's going to throw so when it throws we just want to return false so this function here is just going to return a boolean and because it's an async function of course it's going to return a promise that results in a boolean so let's add this compare password method to our interface we're going to come up to our user document i'm going to say compare password it's going to take a candidate password i'll type string it's going to return a promise of type boolean because we're using 6 this hooked next function doesn't exist so i'm just going to remove it so this is our user model complete let's move on to the user controller in our source directory let's create one new folder and i'm going to call this controller and inside of controller i'm going to create a file called user.controller.ts inside of usercontroller i'm going to export one function and that one function is going to be a handler to create a user i'm going to say export function create user handler and all handlers are going to take the same input so they're going to take a request and a response let's import those interfaces from express and we can type our request here and our response we're going to add a try catch block and i say cont user equals await and we're just going to add a comment here and it's going to say call create user service we still need to create this service i'm going to import our logger from our utilities folder i need to comment this out completely i'm going to call logger dot error and i'm just going to pass in our error here then i'm going to return res dot status it's going to be a 409 and 409 means conflict so we're going to assume that if this function throws that it's thrown because it has violated the unique restriction on the email field in our user model so that means that a user with that email has already registered i'm going to send back our a message let's create this user service so in our source directory we're going to create a new folder called service i'm going to create a new file i'm going to call this user dot service dot ts and inside of user service i'm going to import from mongoose then i'm going to import one typescript definition and this is going to be document definition i'm going to export async function and this is going to take some input and the inputs type is going to be document definition and document definition is a generic type so we can import our user document next we can add a try catch and we can return await user and we can import this from our user model dot create and we're going to create a user without input finally we're just going to throw new error then we're going to pass in our error here i'm going to type our error as any and i forgot to name this function it's going to be create user let's go back to our controller and we're going to call this create user function and our create user function is going to be called with our request.body and create user handler needs to be an async function so let's go back to our routes and under our health check route i'm going to create a new route for creating user i'm going to say app dot post the route is going to be api users and this is going to take our create user handler and we need to import our create user handler from our controller so the problem with this is our requests are not being validated at all so we need to add some middleware here that is going to validate this request user handler otherwise we're just passing whatever the user gives us into our user service and that is a problem for a few reasons one if they pass in the wrong thing they're not going to get what they expect back and secondly we want to be validating what a user is passing into our database in our create user handler let's return our user so let's create a new folder and this new folder is going to be called schema and inside of schema i'm going to create a new file called dot user.schema.t and this schema here is going to be used for all of our user endpoints i'm going to create a new constant so i'm going to say export const create user schema is equal to and i'm going to import object from zod and i'm going to say this is equal to an object and this is going to be the definition for our payload so if we have a look at our middleware you can see that it requires a body a query and params so i'm going to say the body is equal to another object i'm going to say that this requires a name it requires a password it requires another field to confirm the password it requires an email and so name is going to be of type string so let's import string from zod and if the name isn't present we want to return an error so the required error is going to be name is required let's do the same thing for password and on password i'm going to call dot min and the minimum password length is going to be 6 and i'm going to give an error here that says password too short should be six jars minimum so if they enter a password that is less than six characters they're going to receive this error here password confirmation is going to be a string and this is going to be password confirm is required and we're going to do the same with email the email is going to be a string and it's going to be an email and if they don't pass us a valid email they're going to get an error that says email is required if they don't pass us an email at all so if they don't pass as a valid email it should say not a valid email if they don't pass us an email they should get an error that says email is required so we could add some logic to make sure our password and confirm password are the same inside of our controller or inside of a service but zod can do that for us so let's do that in this schema so on our body object i'm going to call a dot refine and dot refine is going to take a callback and the argument is going to be data we say data dot password equals data dot confirm password i'm going to give an error message there's passwords do not match then inside of a path we're going to give a path to confirm password so this is going to construct the error message if our passwords don't match let's go back to our routes and we need to use this middleware here let's import our validate resource i'm going to do it down here and hopefully vs code will import it for me and it's not going to i'm going to import validate resource from a middleware validate resource and validate resource is a function that takes our schema so we need to execute this function here and this needs to take our create user schema so we have a schema for our input so you should expect then that we can properly type our user controller to tell the request body what it should expect let's come back to our user schema and i'm going to create a new type so i'm going to say export type create user input and this is going to be of type type of and typeof is going to come from zod you can see here that we need to import typeof from zod and if we hover over type of you see that it is a generic that expand that extends zod type so let's pass in type of i'm going to pass in create user schema now let's have a look at this type and this is an interface for our input let's come back over to our user controller and if we hover over request you can see that we get here some params and then we have the request body and then we have the response body and then we have the request body so we can pass in our generic brackets here we don't have any params so let's leave that empty we don't have a response body so let's leave that empty but we can pass in our create user schema and we just want the body of this so now let's hover over our request body and you can see that we're getting some good input here so this is telling us that our interfaces don't match because created at an updated app aren't on the create user input so this is a problem both with our schema and with our create user service so let's omit from our user document and we're going to omit two properties created at and we're going to emit updated and the reason we're going to omit these two properties is because we don't need to pass them into this function here they're going to be generated by mongoose when we create and update our documents so the other problem is that compare password exists on create user input so let's come over and let's omit compare password from create user input we need to emit body dot password confirmation so we also need to emit our compare password method so in use document let's also admit compare password because if remember on user document we have this compare password method and we obviously don't want to pass that in so now typescript is satisfied here and we just need to type our error message as any so let's go over to postman and test out this endpoint so i'm going to click send and we should expect an error here because i created this user earlier let's send and instead we see a different area here so we see invalid type expected an object but received undefined so this is a little bit weird and the reason is that we don't have a body parser express requires you to have a body pass in middleware to pass request bodies and so by the time it gets to our middleware our body is in fact undefined so let's fix that by using some middleware that comes with express so i'm going to say app.use and when you call app.use this is going to apply this middleware to every single route that is under this call which is going to be all of our routes because you can see our routes are defined down here so i'm going to say express.json and our server has restarted so let's try this again so our request is just hanging here and so this indicates that we haven't called next somewhere let's cancel that request and my assumption is that it's going to be in validate resource and so you can see here that we're trying to pass the schema but we've never actually called this next function and so our request just sits here so let's call next wait for our server to restart go back to postman and we can send this request again and you can see here we get a duplicate key error and this is exactly what we expected so i'm just going to delete this user and then i'm going to try send this request again so we have another hanging request here and this means that we haven't sent something back to the client and we're looking in the create user service here and so we're returning here and this looks okay let's have a look in the controller and in the controller we're just returning the user this is not going to work in express in express you have to call our response object so let's call res dot send and we're going to send the user back and love it or hate it this is how you have to do it in express a lot of people tend to pick the hated side let's come back to postman and we can send a request and we get our user back this is a bit of an issue we don't want to send back the password so we can omit that from the response you can see here we get jane doe so let's go omit this password so i'm going to import emit from lodash and i'm going to wrap our user in emit and we need to call dot to json on the user and this is just going to convert it to a plain json object and then we need to tell it that we want to omit the password so i'm going to delete our user again come back to postman send the request and we get our user back without a password so the next thing to do is to create our create session handler i'm back to vs code and i'm going to create a new model and this model is going to be for sessions so i'm going to say session dot model dot ts and i'm going to be a little bit lazy and i'm going to copy from the user model and then i'm just going to rename this to session i'm going to import export the session model i'm going to export it as session change this to session schema remove compare password and our pre-save hook so a session schema is going to have a user and a user is going to be of type mongoose.types.object id and it's going to reference the user let's remove this i'm going to remove string and i say mongoose dot schema dot dot object id and i'm going to pass in another prop here and it's going to be ref and this is going to reference our user model i'm going to give this a valid prop and this is going to be of type boolean and it's just going to be default to true and remove password and our schema up here is going to be user and user is going to reference our user documents id field so let's say user document and it's going to be underscore id it's going to have a valid prop and again this is going to be a boolean and i'm going to remove password let's change this to schema document and when i called userdoc it thought that that's what i was referencing let's import userdoc from our user model i'll remove candidate password and this looks good if you want to you can also add user agent and you can use this to store the user's browser that they created that session in this will be handy if you want to list out the sessions in a user interface and tell the user that they logged in on xdate with this browser so we'll make this of type string and we may as well add this to our document as well so let's work backwards from our service up to our model and then to the route this time so let's create a new file inside of service and i'm going to call this session.service.ts this is going to have a few functions in it so i'm going to say export async function create session i'm going to say advanced session equals await session model dot create and we need to pass in some props into create session i'm going to pass in a user id this is going to be a string and i'm going to pass in the user agent which is also going to be a string i'm going to say user is equal to the user id and then i'm just going to pass in the user agent then i'm just going to return session dot to json so now we have a service to create a session let's create a handler for creating a session so come over to controller i'm going to say session dot controller so export async function create user session handler and just like our user controller this is going to take a request and a response let's copy those interface imports from here say request and response and we can type these up so to create a session there's a few things we need to do firstly we need to validate the user's password we need to create a session we need to create an access token we need to create a refresh token and then we need to return access and refresh tokens okay so to validate a user's password let's create a service in our user service for validating the user's password so you come into user service and under create user i'm going to export async function validate password and this is going to take an email and it's going to take a password so let's type these out this is going to be a string and then of course password is going to be a string as well i'm going to say const user equals await user model dot find one and i'm going to find the user by their email address i'm going to say if the user doesn't exist let's just return false from here if the user does exist we want to call our compare password let's create a new constant called is valid equals weight user dot compare password and we're going to pass in our password and remember this password that the user supplied is the candidate password so we can see here that compare password doesn't exist on user but we added it to the interface so let's go have a look at our model and we can see here that our model here should take a generic you can see here model unknown so let's pass our user document into our model here save that and we'll come back to our user service and you can see that compare password now exists on our user object if is not valid we're going to return false otherwise we're going to return the user but we want to omit the user's password so let's call admit and again we're going to import omit from lodash and we could do this in create user as well so we could say const user equals await and then we could return omit user dot to json and we could omit the password and then we can copy this and let's paste it down here so this function here is going to take an email or password and if the password is correct it's going to return the user object and the password is incorrect it's going to return false let's use this so we'll say const user equals await validate password and we'll need to import this from our user service hvs code will do for us and we can pass in our request body and we're going to need to validate this request body so we'll create a schema for that shortly and i'll say if not user because remember if the password is wrong user is now going to be false i'm going to return res.send so res.status 401 dot send invalid u email or password we're going to tell them which one was invalid so otherwise we're going to create a new session so i'm going to say const session equals create session i'm going to pass in the user's id and i'm going to pass in rec.get user agent or an empty string so this is going to attempt to get the user agent from the request object or it's just going to set user agent to an empty string we need to create an access token but we don't have a utility to do this so let's go into our utils i'm going to create a new file i'm going to call this jwt.utils.ts inside of jwt utils we're going to have two functions we're going to have a function for signing a jwt and we're going to have a function for verifying a jwt so to do this we're going to import wwt from json web token and we're also going to import config from config so we're going to assign the jwt with a private key and then we're going to verify the wwt with a public key so if we come into our config you need to set a public key and a private key i just generated these online and you should use your own public and private keys and so as the name suggests this public key you don't need to keep secret you can send that out into the public and anyone that holds this key can validate jwts that were signed by this private key but you want to keep this private key safe and you only want to expose it server side because anyone that holds this key can generate jwts for your system and therefore has access to any resource in your system let's come back to our jwt utils and i'm going to say const private key it's equal to config.get private b the const public key is equal to config.get and this is going to be public key and let's add some types of these these are both going to be strings one thing to note about these keys if you go to paste them into this file here you may have spaces here like this you need to remove it these keys need to be in this specific format and you're going to get some really weird errors if they aren't in this format so make sure that they look exactly like this so we need to export this function here our sign jwt is going to take an object and this object is the jwt's payload and this is just going to be of type object and we're going to take an optional parameter of options and this is going to be of type jwt dot sign options or it can be undefined and then i want to return jwt dot sign and we want to sign our payload and when you want to use our private key to sign the payload and i'm going to pass in some options so we want to spread our options onto here because we also want to provide an algorithm option so i'm going to say algorithm and this algorithm is going to be rs 256 and this is going to allow us to use public and private keys problem with this is options could be undefined so let's check that it is defined before we spread it we can do that in a little function that looks like this while we're here let's write our verify jwt function so our verify jwt is just going to take one token and that's going to be a string and when jwt dot verify can't verify the token it throws an error so we need to wrap this in a try cat block and say const decoded equals jwt dot verify and pass in our token and i'm going to pass in our public key if the token can't be verified i'm just going to return an object i'm not going to throw this error i'm going to say valid is false expired is going to be equal to error dot message equals jwt expired i'm going to say decoded is null if the jwt can be decoded i want to return an object that's very similar to this so i'm going to say valid is true expired is going to be false and decode it is just going to be our decoded object let's type error as any and we are calling this e not error so head back over to our session controller and continue making out our create session let's say const access token is equal to so to create an access token we're going to use our sign jwt utility so i'm going to call sign jwt and our payload is going to include our user and it's also going to include a reference to the session i'm going to say session.underscoreid i'm going to pass in some options here i'm going to say expires in and this is going to be an access token time to live that we want to store in config let's come over to our config here and i'm going to create a new property and this is called going to be called access token time to live i'm going to set this to 15 minutes i'm going to create a new property here as well and call this refresh token time to live and this is going to be one year you might be thinking if the access token is 15 minutes does the user need to log in every single 15 minutes and without the refresh token that would be the case but when the user makes a request with a valid but expired access token we're going to return a new access token for them if the request also includes a refresh token so we can have a look at our diagram here and this is going to say the users make an authorized request so is the access token valid no just return an error if it is we're going to say has the access token expired yes is a valid refresh token included yes we're going to issue them a new access token and then we're going to process the request and to process the request we're just going to go to the route handler so we're going to do this here in some middleware so let's get this expired in so i'm going to say config.get and the property that i'm going to get is access token time to live and we need to import config let me say import config from config and let's add a little comment here that just says 15 minutes to remind myself that this token will only live for 15 minutes you can see here that id does not exist on session and that's because if we have a look at create session this is an async function and we need to await this the session here would be a promise but we actually need it to be an object so now that we have our access token let's create a refresh token so i'm just going to copy access token i'm going to call this refresh token then i'm going to return res dot send i'm going to pass in the access token and refresh token let's go over to postman and try this out so we've created a user now we can create a session let's send this request and cannot post api sessions so we need to create a route handler for this let's come over to routes and we can copy our user's route you can say this is going to be post api sessions and we're going to need to create a new schema for this but we can call the create session handler let's create a new schema so i'm going to say session.schema.ts go to import from zod and we're going to need object and a string i'm going to say const create session schema is equal to object now object is going to have a body and the body is going to be an object we're going to have an email which is of type string and we're going to have a password and this is going to be i'll type string let's export this as well and we'll add some error messages here so required error the email is required and for password we'll add the same thing required error password is required and we'll replace this schema here with our create session schema and vs code has imported that for us come back to postman send the request and we get an access token and a refresh token back so let's try post this without a password and see what it says you can see here we get an error and it says password is required try the same thing for email and we get email is required the third route that we're going to create is one for getting all the sessions that the user has so i'm going to say app.get slash api sessions this needs a slash in front of it and we're going to have a get user sessions handler so come into our session controller and we'll make a new function here called get export async function get user sessions handler we can copy our request and response from the function above and we need a service for getting these sessions let's go over to session service and say export async function find sessions this is going to take a query and our query is going to be of type filter query and filter query is a generic that's going to take our session document so we need to import session document from the session model and we're just going to return session model dot find we're going to pass in our query and we're just going to assume that every query to this model is going to require.lean and lean means that it's not going to return all the functions on the object it's just going to return the plain old object the same as to json let's say const user equals am i stuck because how do we know what the user's id is so ideally the user will be on the request object so let's go create some middleware to add the user to the request object i'm going to go create some middleware and this middleware is going to be called d serialize user i say const d serialize user and this is going to take three arguments first it's going to be pressed response and next so express middleware is basically a route handler there's essentially no difference between actual middleware and the route handler they all just take the request response and next so you could just return any business logic from here and then call res.send but that would mean that it's just a route handler so let's type out request we'll import our interfaces from express and next function we need to get our access token from the request headers so i'm going to import a utility from lodash called get and this is just going to make it a little bit safer accessing a property that we don't know if it exists or not so say const access token equals get and pass in the request and pass in headers dot author as action otherwise it's just going to be an empty string and i'm going to call dot replace and i'm going to pass in this little regex here and i'm going to replace that with an empty string so at the start of an authorization token you're going to have the word bearer and the word bearer basically means the bearer of this token gets access to the system we want to remove that word from the token so we're going to say if not access token i'm just going to return next otherwise we're going to try verify this access token so i'm going to say const equals verify we need to import verify jwt from our jwt utils let's call verify jwt i'm going to pass in our access token and verify jwt is going to return valid expired and decoded we need to get decoded and we need to get the expired boolean so i'm going to say if decoded and we're going to have decoded if there is a valid jwt i'm going to attach the user to res.locals.user so i'm going to say res.locals dot user equals decoded and then i'm just going to return next so for now this is good enough this is going to attach the user to res.locals.user so we can come back to our session controller and we can say user equals res dot locals dot user and we can get the id as well then i say const sessions equals await and call find sessions and i'm going to say user is equal to our user id and let's change this property up here to be user id and i want to go only get the valid sessions i don't want to get any expired sessions or sessions that are marked have been marked as invalid i'm going to call return res dot send sessions let's come back to postman and we can try get these sessions and we get a 404 and we know we get a 404 because we haven't hooked the route up to our handler let's send this and again we have a request that's just hanging so we've just introduced some middleware but the middleware doesn't actually ever get called so to make sure this middleware gets called on every single route we're going to come back to app.ts we're going to import our middleware i'm going to say app.use and i'm going to pass in deserialize user and we need to move this down and just like express.json that handles the request body d serialize user is going to be called on every endpoint for every request and it looks like we can't import it so so it looks like we need to export d serialize user let's come down to the bottom here say export default deserialize user and app.use not app.user the server has restarted so let's try this again and again we are still hanging and we have no errors in the console so let's go into d serialize user and let's try start debugging so i'm just going to log decoded and a string before this so we know that the decoded token and then the next path is going to be our handler so i'm going to log the user id and i'm just also going to log these sessions to see where we get up to basically what i'm trying to do is trace the request through our application to see where it's hanging you can see that it could also be hanging in this find session let's send this request again and we get decoded null so decoded is null and we're returning next if decoded is true so we just want to provide a default return here and we just want to return next so if none of these conditions are met we're just going to get to this point here and then we're going to return to come back and we can try this again we're still hanging and we get this cannot read property type id of undefined so this is because in our session controller we're accessing res.locals.user.id but user is undefined so to prevent this from happening we need to create a third piece of middleware that is going to validate that the user exists for given requests and i'm going to make this middleware called require user dot ts i'm going to say const require user equals a function and we're going to export to fold so we don't forget to do this again we're going to do the import request response and next function then we're going to do request is i'll type request response use of type response and next here's a type next function so we're going to try get the user say const user equals res locals dot user then we're going to say if not user we're going to return res.send status 403 sorry this is response so when this middleware is used we want to make sure the user is required so we're only going to have this on routes where we actually require the user so it's safe to return a 403 error here otherwise we're just going to return next so if they get to this path down here it means that the user is on the response object because in d serialized user we put the user on the response object because they had a valid token let's come back over to our routes so we can use this middleware so in get sessions i'm going to type require user and i'm going to save that and so now because our decoded token is null we should expect to see a 403 error for get sessions and we do the problem is is that we shouldn't be seeing a 403 here because we've created a session because we logged in so let's try log in again here you can see we get an access token and refresh token we can send the request and we get an empty array of sessions so why is that and the problem is that we're looking for sessions that have valid false when we really want sessions where the valid is true remove the console.log and send this request again and we get all of our sessions back let's remove these console logs so the last route that we want to make for a user is for them to delete a session and log out let's create a new handler here and we'll call export async function delete session handler this is going to take a request and response so i want to get the session id so i could say const session id is equal to res.locals.user.session so remember it's safe to access this if we put the require user middleware in front of this handler so we need to update a session and then we need to return a response so let's return res.send status 200 and in fact we could just send here access token is equal to null and refresh token is equal to null and then the ui can handle this accordingly so let's go over to our session service and i want to create a new function so export async function this is going to be for updating a session so to update we're going to have a query and this is so we can find the session that we want to update and this is going to be of type builder query and we're going to pass in the schema document like we did before then we're going to have an update and this is what we want to update the session to so this is going to be an update query and again we can pass in our session document and i just went through and renamed all these because they were named schema document but they should be called session document and i can return session dot update 1 and i want to pass in our query and our update and this is session model not session let's come back to our session controller and i want to say await update session and i want to update the id session and i want to set valid to false so we're not going to delete the session we're just going to set it to false so then when the user tries to use it again they're not going to be able to use that session so let's call delete session and we get an error here and we forgot to add the route handler again so i'm going to copy the handler to get the sessions and i'm going to make this a delete method i'm going to say delete user it's called delete session handler so i'm going to say delete session handler and then we're going to need to import that handler and we need to have this require user middleware here as well let's send this and we get refresh token null and access token null so now if we get sessions we should only expect to see one in this array and we do so this is all of our user routes finished the only problem is is that we aren't handling refresh tokens so let's go back to our deserialized middleware and issue a new refresh token if the user's token has expired i'm going to come back to deserialize user so if the user has a token that's good we can just return next but we want to say if wired and they have a refresh token which we need to get out of the headers so let's do that now so where we get this access token i say const refresh token equals get i'm going to get it out of the request and i'm going to get it from headers dot x refresh so if the token is expired and they have a refresh token and want to be able to re-issue them an access token so in our session service let's create a function for reissuing an access token so i'm going to say export async function re-issue access token and this is going to take one object and this is going to be our refresh token and our refresh token is going to be a string so i'm going to try decode the refresh token because we need to make sure it's valid i'm going to say verify gwt i'm going to pass in the refresh token and i say if not decoded or we don't have a id on the decoded refresh token because the id is going to be a session id so we need to import get from lodash and we need the session id to make sure this session is still valid before we issue an access token we're just going to return false if those conditions are not met then we want to get this session so i'm going to say const session equals weight session model dot find by id and we're going to get decoded dot id and to keep typescript happy we can say get let me say if not session or if not session dot is valid if so if the session has been set is valid false we don't want to issue a new access token so for this case we're just going to return false again an invalid doesn't exist on our session and my suspicion is because we haven't added the generic to the model let's add this session document to our model then we want to find the user we're going to call find user so we need to create a find user service so let's come over to user service export async function find user find user is going to take a query and this is going to be a filter query and our filter query is going to take our user document i'm just going to return user model dot find one and we're going to find with our query and let's also call dot lean on this let's come back over to our session service and say find user and we want to find the user by the id and we can find the user by the id because it will belong on the session and is valid is still not here go over to our session [Music] model and this is called valid not is valid let's change this to valid and we need a return so if we don't have a user we also want to return false but if we do have a user we want to create a new access token so let's go to our session controller and we created access token in create user session so let's just copy this code here we can come back to our session service we can create a new access token and then we can return the access token so now we can use this reissue access token inside deserialize user to get a new access token and do all the checks required to make sure we are allowed to get a new access token so i'm going to say const new access token is equal to a weight reissue access token and we need to pass in our refresh token so if we have a new access token we want to set a header with that new access token so we can say res dot set editor i'm going to call this x access token and the value is going to be new access token so we also need to add the user to res.locals so d serialize user now needs to be an async function so i can say result equal to verify jwt and we can pass in our new jwc that we just created and then we can attach this to res dot locals dot user is equal to result dot decoded now we can return next so let's take a look at what this function is actually doing so we get the access token and refresh token from the headers there's no access token we just return next if there is an access token we try to decode it and if we can decode it we attach the user to res.local.user and we will turn next out of this function if the token has expired and there is a refresh token we check that the refresh token is valid and we issue a new access token and then we set the new access token on the header of x access token we decode that access token and attach the user back onto res.locals so the reason we do this is because if they send a request with an expired access token the request flow is just going to continue as if they sent the request with a valid access token given that the refresh token was valid so let's test out the refresh token flow so i'm going to come into config and i'm going to change my access token time to live to -1 and this is going to generate an expired access token for us so i'm going to come over to postman and i'm going to create a session and so this access token that we have now is going to be expired so now i'm going to change this back to 15 minutes and we can open up the console in postman and i'll clear this out and i'll make a request to get sessions so we have a problem here and it's forbidden so this means that our access token is not getting regenerated the issue is that we're creating the refresh token with the access token time to live we need to create this with the refresh token time to live so let's come over to defaults copy this and we'll paste in the refresh token time to live let's change this back to -1 so we can generate another expired access token and a non-expired refresh token this time so i'm going to create session come back i'm going to change this to 15 minutes and let's try get our sessions again so we're still getting forbidden and the reason for that is because in our reissue access token when we look for the session we're trying to find it by the id when we actually look at this token we can see the decoded has a session id so we need to say decoded session and we can change this here as well so let's go back and try this again and you can see that our request is successful if we have a look in the response you should see that in the response headers we get an x access token and we also log set new access token and that is because we have this tests and it tries to look for x access token in the header and it's going to set a new access token for us if that is present in the header let's move on to creating a product let's come into services and i'll create a new service called product dot service dot ts and i'm going to come into models and i'm going to make a new model called product dot model dot ts in our product model i'm just going to copy session i'm going to paste it into product model and to remove these things here we can also remove those imports from session as well so i'm going to import from nano id and i'm going to import custom alphabet i'm going to say const nano id equals custom alphabet and this is going to allow us to create a custom alphabet for our nano id so the alphabet that i want to use is all the characters from a down to z which is this is not all of them u r s e u v w x right said and we also want numbers zero to nine so instead of the session document we're going to have a product document this needs a 10 so this id is going to be 10 characters long a product is going to have a title there's going to be a string it's also going to have a user and this is the user that created the product so we're going to have a description it's going to be a string we're going to have a price it's going to be a number we have an image going to be a string and then we're going to have created that it's going to be a date and updated at there's also a date change this to product schema and product is going to have a product id and you don't have to do this if you don't want you can use mongoose's generated id if you like so mongodb's generated id this is going to be a type string it's going to be of type required true going to be unique true and then we're going to default this to a function the function is going to return a string so we're going to prefix this with product underscore and then we're going to call nano id and this is going to create an id that is prefixed with product and i'm going to call nano id and that's going to add an id that's generated from our custom alphabet then we're going to have title going to be of type string and it's going to be required i'm going to have several other properties i have a description we're going to have a price and an image and the price is going to be a number to export this as product model and as product document and then we're going to export product model let's come back to our product service and i'm just going to make all the services that we need so we're going to need a service for creating a product we're going to need a service for finding a product we're going to need a service for find and update a product and we're going to need a service for deleting a product so this is going to be product model dot create and it's going to take some input and let's return this now input is going to be document definition and this is going to take our product document and we also need to emit created at and updated that so let's admit then we'll add created at and updated at find product is going to take a query and this is going to be of type filter query and we're going to pass in our product document and it's going to take some options these are going to be query options and we're just going to default this to lean true let's return product model dot find one query projection is going to be an empty object and then options find an updated product is going to take a query it's going to take an update property and it's going to take options this is going to be of type query options update is going to be of type update query and this is going to take our product document and then query is going to be this filter query we're going to return product dot find one and update and then we're going to pass in query update and options finally delete is just going to take a query and this is going to be of type filter query then we're going to return product model dot delete one and we'll pass in the query and product here should be product model and this all looks pretty good let's move on to creating our controllers i'm going to create a new controller called product.controller.ps i'm going to export async function create product handler it's going to take our request and our response and we can import these from express and this is probably the last time we need to do this which is nice i'm just going to copy this function and change this to update and i'm going to create one for get and i'm going to create one for delete so let's also create our schema so i'm going to say product dot schema dot yes so if we have a look at the handlers we have we have create and update and these two are going to take pretty much the same input and then we have get and delete and then these two are going to take pretty much the same input so these are going to have the body of a product and these are just going to have some params with a product id so let's create one constant and we'll call this payload and this is going to equal an object make an import from zod we're going to import object number string and type of then we can add the body and the body is going to be an object we can add title string description and it's going to be a string a price going to be a number and an image is going to be a string add some validation to these required error title is required i'm just going to copy these and i'm going to change this to image price and description so we want our description to have a minimum length of 120 characters i'm going to say dot min and then i'm going to say 120 and then i'm going to give an error here that says description should be at least 120 characters long let's create a new constant here called params equals and we're going to give it params this is going to be an object and the params that we're going to take is just a product id so this is going to be of type string and we're going to have a required error it's going to be product id is required let's create the schemas that we need so we need a schema for creating a product this is going to be an object and then we can spread our payload update is going to be the same but on update we also want our params so let's spread our params or delete we just want our params so we can remove payload from here and then get we just want params as well so let's export all of our type so i'm going to say export type put equals type of type of probably this four times so we're gonna have one for create update read and delete so now we have all of our types for our crowd operations we can come back to our controller and we can add these types in so for creating a product the request is going to have no params no response body and we're going to have create product input and then we just want the body for updating a product we do have params we're going to have update product input and then we're going to fetch the params let's change these to uppercase to match the rest and i'll fix this create here and i'll import update we're getting a product we're also going to have param so we can copy this generic here and for deleting it's going to be the same so let's work on creating a product handler so say const user id equals res dot locals dot user dot id and we're going to make sure that the user is required for creating products so this is okay to fetch prompt body equals rec.body the const post equals await create product this is imposed this is a product and i'm going to spread the body and i'm going to say the user is equal to the user id now return res.send and we can send out the new product you probably also want to add some error handling around creating this product here as well so you can wrap this in a track hatch block so let's get the user again from the response and i'm going to say product id is equal to rec.params dot product id and we get some helpful hints here because we've typed this product id up here the const update equals rec dot body let me say const product equals await and we're going to find the product by the product id and then we're gonna say if not product then we're going to return res.send status 404 then we're going to say if product dot user is not equal to user id we're going to return res.send status 403 so the user that created the product is not the user that's trying to update it so we're going to return a 403 and finally we're going to update the product wait find and update product we're going to update by the product id we're going to pass in the update and we're going to pass in some options so we want new to be true to return the new product i'm going to return where's dot send updated product so let's copy the body of this function here and we can move this to deleting a product and we're going to call await delete product and then we're going to remove these properties here so this is pretty much the same we're going to get the user we're going to get product id from params we're not going to get the update body then we're going to find the product we're going to make sure it exists we're going to make sure the user that created it is the user trying to delete it finally we're going to delete the product and then we're going to res dot send status of a 200 move on to our get product handler i've done update in the get handler i need to move this to the update handler we're going to get the product id from params we're going to try find the product find product by product id and we're going to say if the product doesn't exist we'll return this 404 status otherwise we're going to res dot send product and i'm going to return this so let's hook these controllers up to our router you can say app.post api products we're going to have multiple sets of middleware here so we're going to require user and we're going to validate resource and validate you resource is going to take a create product schema then we're going to use the create product envelope i'm going to copy this route here i'm going to change our update to a put the resource path is going to be the same this is going to be our update product schema and this is going to be an update product handler to get a product we're going to have a get request we're not going to validate the user so we only need one bit of middleware here but we are going to validate the input so it's going to be read product it's going to be get product schema and this is going to be our get product handler and then delete is going to be similar but we do want to validate the user let's add this array of middleware back and this is going to be delete product schema so we need to make sure that all these schemas and handlers are imported so it looks like vs code has done some really funny stuff here we can fix this up so once you've fixed all the imports the server should start successfully and we can go test all of our endpoints so i'm going to try create a product you can see here that our product was created successfully and our product id should have been updated as well in postman we cannot get products so let's try this we have get here but we need to add some params to this so we're going to say slash and then we're going to add product id and we need to do the same for delete let's send this get request and we get our product back here let's try update the product and can output to this product and i assume this is because on our put requests we also need to add the product id as a route parameter so let's try update our product so we're getting a forbidden error when we try updated product and if i have to have a guess at why this is the issue it's because product.user is going to be a mongoose object id where user id is just going to be a string let's cast this to a string let's also do the same where we check that the user is the actual user that created it in delete so we're using a triple equals here and so the types need to be the same as well and we can update a product now finally let's delete a product and we get an error when we try delete so let's have a look at our delete route and we're missing a slash here and our delete route works so this is our express api complete so if you like the video please make sure you leave a thumbs up and make sure you subscribe and turn on the notifications so you can see these next videos as they come out as well thank you for watching and i'll see you in the next video [Music]
Info
Channel: TomDoesTech
Views: 13,468
Rating: undefined out of 5
Keywords: coding for beginners, coding, typescript, javascript, nodejs, tutorial, programming, restful, api, backend, server, http, https, rest api, express, ExpressJS, Express.js, node.js, node, MongoDB, Mongoose, Mongoose typescript, zod, zod validation, how to build a rest api
Id: BWUi6BS9T5Y
Channel Id: undefined
Length: 129min 19sec (7759 seconds)
Published: Tue Oct 05 2021
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.