Build a CRUD API with TypeScript, Express, MongoDB, Zod and Jest

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
[Music] hello friends welcome to coding garden in this video i am going to create an express api with typescript so if you watched the last video i gave a basic introduction to all of uh the things you'll need to know in typescript in order to get started with typescript it wasn't a deep dive it was really just all of the the base understanding of typescript and really enough to get you started and enough to to allow you to understand what we're going to do here uh when we create this express api with typescript now in this github repo i do have links to all of the stuff we talked about last time in terms of how things work but down here i talk about the fact that we're going to use a project generator now we could start from scratch but then this video would be an hour longer unnecessarily so i'll point you to the github repo that i have called express api starter ts so this is a super basic starter template for an express api with typescript um it this doesn't assume any database this doesn't uh add a bunch of extra stuff this is only only the things you need and nothing more for a basic express api with typescript and then you can put all the extra stuff on top of it that you need to and that's what we're going to do today so if we run this command in a directory that's going to clone this repo down and then we can get coded for get coding from there and i will give you a quick rundown of all the files that are in there so i'm doing this from within the intro to typescript repo so i'm actually going to make an examples folder first and then inside of that examples folder i'm going to run this command and then the directory i'm going to call i'm just going to call this express api and so this will essentially clone down that repo do an npm install of the dependencies and then it's ready to go so now if i go into express api and do npm run dev we have ourselves a basic express api written with typescript running on port 5000 great great let's take a look at the code so if you look at the readme i talk about some of the dependencies we have in here so some of the middlewares we have for like error logging and header protection and env loading and cores and then also some of the development dependencies we use as well i would say the main thing you need to worry about in the root folder is probably the package json if anything this is where your dependencies are going to get listed if you want to add more and you could change the start scripts but pretty much all of these other files are just configuration files that you can leave as is if you look at the ts config file this is where you could add more typescript settings if you wanted to but these are some sensible defaults essentially we're targeting es next we're using common js we have an output directory of dist and we're including all typescript files in the source directory and typescript files in the test directory the test directory right now just has super basic tests uh and it's using jest and a thing called supertest so essentially we can load in our express app and then run requests against it and then expect against the responses to make sure that it's responding in the right way and then if you take a look in the source directory we have our index.ts this is our main entry point so it brings in our express app and then starts it on a specific port if you look at app.ts this is our express app it loads all the middlewares has just a basic response handler and then we have an api router where all of our api routes get loaded and then we have a couple of middlewares as well so if you look in the middlewares file this has a middleware which handles when there's a route that is not found and then this has also an error handler which is going to be our catch-all error handler so any errors that happen will eventually all make it to the same place and we're going to handle all those errors in in one spot okay if you look in the api folder this is where we're going to put our routers if you look at index.ts this is just the main one and then it's loading an example router emojis that just returns an array of emojis and if you look at app.ts we show where this all starts so slash api slash v1 loads the api router the api router then loads the emojis router and so that means we can access this at api v1 emojis so if we make a request to api v1 emojis there we go we get our emojis back but that's just a quick whirlwind overview of everything the first thing we'll do is just talk about where we're actually using typescript right now so if you take a look at app.ts you can see there's some weird stuff going on right here and this is essentially describing what the response of this specific request handler is going to be so message response is an interface i defined that is just describing an object that has a message string so this is in the interfaces folder it's just a super basic interface it describes an object with a message property that is a string and so what i'm saying right here is when a get request comes into my app on the slash route i am going to be responding with something that is of this interface message response um and where this is handy is now when i do res.json i'm going to get typescript support so if i try to specify values here that are not on that interface i get a typescript error so this tells me that banana string message string is not assignable to parameter of type message response so this right here um is instantly giving us the the typescript support that we want because it's helping us make sure that we're passing in the right things and so essentially every api handler we create we're going to create an interface that describes the response there are some other arguments that go in here as well the first one actually defines the params or the parameters in the in the url and so later on when we show an example of like get a to do by id we're going to define an interface that says our params should have an id property of type string right now this is saying that we don't have any params at all so if i do rec.params dot i get no type completion because i've told it that there shouldn't be anything on here it should just be an empty object so it doesn't auto-complete any properties this is really the benefit of using typescript here now i'm going to show two ways of doing this you can do it on the handler itself uh using the the brackets um uh basically this get function is defined as a generic function and we're defining the types here the other way to do it is to specify it on the specific request and response objects but i'll show that when we get there okay before we install dependencies i just want to talk about how you can find dependencies for your typescript project so if you take a look on the typescript lang website they have a search page where you can search through modules to see what's available so we specifically are going to be using mongodb and so if i search mongodb on here this tells me that all i have to do is install mongodb and it already has the types built in another way to tell this is if you look up the package on npm you'll see a little typescript icon next to the to the package and if you see that that means that you only have to install that package you don't have to install any extra types or anything like that we're also going to be installing a library called zod and it is written in typescript so it supports typescript but an example of something that actually doesn't have built-in typescript is express um so if you search for express you'll see that it actually wants you to install install two different things and as a part of this template i've already done that for you so if you look in the package.json we have express as a dependency but we also have types expressed as a dev dependency and we also have the types for our course module and for morgan all as dev dependencies but if i were to search for express on this website it does tell me that hey you need to install this separate types package and if you look at express on npm you can see that it doesn't have a solid blue box these have solid blue this one's not solid blue and this tells me that i need to install a separate type package so that's good info to know and it's also a good info to know that all of these extra types come from a project called definitely typed so if you're trying to use a javascript library that doesn't have typescript definitions built in people have done the work of submitting those types to this project so that people can bring them into their own project without needing to make a pull request or modify the original project to include the types directly so that's all we need to know about that we're now going to install zod and mongodb um these two dependencies because this is how we're going to interact with our database so let's do it so in this folder i'm going to do an npm install of zod and mongodb great and now we'll start to use it so our first lesson comes in uh on how to structure our folders here because technically i could create a folder here called like models and then this directory could have like user.ts like that's the user model and then let's say we're making a to-do app which we are i guess i didn't mention that but we have a to-do model and then let's say we also have um i don't know the banana model right those that's all in that folder and then we would also need api routes so then we would have some folders over here so we have uh like the banana routes over here and then we have the uh to do routes over here and as your application starts to grow this gets really messy um in that it's it's it's hard to bounce between these folders uh especially if you're working on things of the same type right so let's say i need to update the banana model and then i need to go back over and update the banana routes now i'm bouncing between folders um so the way i like to do this is to put it create a folder with all of the things for a given feature inside of it and let me show you what i mean by that so let me get rid of all this extra stuff and so now in the api folder i'm actually going to create a folder called todos and all of the things related to two to-do's are going to go in here so it's going to have my router my model and schema any sort of validation any custom functions if everything related to to-do's are going to go in this specific folder and also maybe even specific interfaces to to-do's are going to go in this folder and that's nice because now what i'm working on to do is i can just go in this folder and then later on if i'm working on another feature i can go to another folder so we're going to start here and i'm going to create a file called to do.model.ts so this is where we will define our basically an interface that describes the to do's that we're going to be having in this api and then we'll also set it up for a mongodb collection so to define our schema we're going to use a thing called zod so i'm going to do import star as z from zod so zod is a schema validation library built in typescript and it works really well with typescript so if you've ever used joy or what was that other one yup if you've ever used joy or yup it works in a very similar way but what's cool about it is it can also generate typescript interfaces and i'll show you what i mean by that so first of all let's describe a to-do using this library so i'll say to do must be an object like this and then a to do has a content property that is a string so if i do z dot string i now have a string validator so this creates a validator i can pass an object to it and if the content property is missing or is not a string it's going to throw an error i can also have like a done property and we'll make this a boolean i can also do things to say like this must be at least length one so you can't pass in an empty string for content so just like that i've defined a validator for the to do's that i'm going to be working with and built in i get this function called parse and i can pass in an object like this so let's say i try to pass in an object with a content property continent content property that is an empty string the result i'm going to get back has uh the actual parsed value um and if the parsing fails this will throw an error and the error will say hey content was was not long enough so this is really great because now i can i can validate things coming into my api awesome the other thing i want to be able to do is have an interface that describes this type and we can do that with zot as well so if i take if i say type to do is equal to z dot infer type of to do now i have an interface um let's export so i'll just say export default to do but now to do is not only a a validation function and a parser it also is an interface so i can use this interface uh when i'm working with my api responses when i'm working with my database models to say that when i'm inserting here it must have a property called content that's a string and a property called done that's a boolean so here we've set up our basically our our basic interface um and for now let's use it so i won't even set up the db stuff let's actually just create a to-do route that describes that it will respond with an array of to-do's so let's do that now so here in to-do's i'm going to create a full a file called to do.route.ts and really while we're here i'm going to pluralize this because we have to dos.model i'm also going to have to dos.route cool and this is where i'm going to define an express router so we'll import in router capital r router from express we'll create ourselves a little to-do router like this and then we'll export it [Applause] like that uh and now let's define our out so let's define a route that returns an array of to-do items so i'll say router.get on the slash route we get the request and the response and then we're going to respond with an array um and we'll leave it just like that for now because i want to go ahead and add some types so i mentioned earlier that there's another way of of typing your request handler you can specify it here so i know that there are no route route param so i could do empty object and then i know that i'm responding with a to do so i could import that to do interface we've defined here um but i actually could say that i respond with an array of to do's like that so this is saying that this route will respond with an rate of to do's right now i'm just responding with an empty array but let's try putting some data in there so if i say content um i say wow or let's say learn typescript like this right now i'm getting an error because we're missing the done property it's required in the type and once i've fixed those type errors then it comes back like that let me show you the other way to write this we can write it using generics on the request and the response objects so express the types in express also give us access to response and request and then we can use those here so i can say that rec is of type request and that res is of type response but these can also be generic so i can pass in the the params here and let's see the first argument is response body so if i say to do array here now we have typing here that says when i do res.json it should be an array of to-do items like that so you can do it one way or the other i think what we're probably going to do most is this because we actually will define these functions in a separate file and when we're in a separate file we don't get the automatic inferred types from doing a dot get um so for the most part this is how we're going to be typing our requests and our responses okay let's test out this route so our app should still be running yeah it's still running on on port 5000 and if i request api v1 to do's it was not found because we need to mount it so uh that makes sense so if i go over here to my api router i need to pull in the to-do's router so i'll say import to-do's from to-do's slash to-do's route and we'll use it so i'll say router dot use one slash to do's i want that specific route and actually i want to pluralize it as well i want to do to-do's routes i'm making a lot of changes here but it's my app i can do what i want so i called that routes um and then that got updated to routes as well cool so now that it's mounted here i should be able to go to api v1 to dues and and we get access to it here great quick aside someone in the chat asked if it was possible to do default values on our schema here and it is and i think it makes sense to do it here so we can say that the default value of done is actually false and now it makes it so that i don't have to specify done but it'll set it to false whenever it's parsing in the object so i think that makes makes sense to do there all right the next thing we're going to do is write some tests for this this basic route that we've written here um we this repo comes generated with some tests so you can see some examples in there but what i'm going to do before we write our own test is i'm going to restructure things but just like i like to have things that are similar together i actually like to put the tests there as well too so i'm going to move away from a separate test folder and move into having the tests right next to the thing that they're testing so this api.test just tests um the uh the main route that it that it returns the api response and then this app.test just tests like the the root uh get route and it also tests that we have a 404 not found so now i can delete that test folder um and the way jest works so just as what we're using for our tests like this it will find any file that has the name test in it so we don't need a separate test folder this should still work so i'm going to kill this for now and then just do an npm test and it should still pick up all of those test files and ideally all of the tests are passing because i haven't changed anything i've only added new things oh so of all these test files we do need to update their um their imports oh these files are not saved save the files when when i drop them into the folder i believe vs code updated those paths for me but i for whatever reason didn't save the files great so now we have some some some passing tests awesome i think i do want to split these out though so if you look in the api tests i had this test in there and then i also have this example emojis test first of all let's just get rid of that emojis test and then let's also just get rid of the emojis router and so now the only router that we're dealing with is the to do's router great let's run the test again well i appreciate that i appreciate that shark turn up you said it like right after i did it save save save save okay um and then now i want to test file for my to-do so in the to-do's folder i'm going to create another file i'm going to call it to dos.test.ts and we'll copy this basic test here and then modify it to match what we're trying to do in this case we need to require two directories up to grab the app okay so i'm going to describe a get of api v1 to dues and this should respond with an array of to-do's so um this library supertest the way it works is you basically wrap your express app um and then you can poke and prod at it and make requests against it and then test against those requests i will mention like in terms of the setup that i have here it is important to have your app your express app defined and exported from its own file you don't want to mix this with what we do in index because we want to be able to test the app the express app itself standalone separate from starting it up on a port because that allows us to do things like this now we can just take that standalone app wrap it in supertest and then do some things with it so i want to make a request to api v1 to dos i want to get application json back and then i want to expect that the content type has the word json in it um and then we'll do it like this expect that i get a 200 status code and then i can just poke and prod at the response i guess we could just test if it has a length property so let's say expect response.body dot to have property length so make sure that we're we're getting back an array as the body and then expect response.body.length to be one because right now we're just returning an array with length one so right now you can see that like we have this done function and technically i could call the done function right here and that lets jess know that i'm done doing all the stuff that i'm doing the other thing we could do though is turn this into an async function and then just return the the call here like that um and now because this returns a promise the moment this resolves just knows that i'm done with my test so this is just a basic test that makes a request to the to-do's endpoint make sure that it has one thing inside of it and then we also could do some expecting on the body the first thing in the body itself so expect that the first item in the array to have property content and then also expect it to have property done all right let's run our tests see what happens cool and i have broken the cardinal rule of test driven development in that i've written a passing test before seeing that it was failing um we could make it fail we can make it fail um just to know just just so we have our have our our sanity here that i actually am testing the right thing and it's doing what i expected to be doing um so in this case the length of the response is one i was expecting it to be two it should actually be one yeah yeah and that's if we're doing tv we're not going to do tdd we're going to we're going to build out the routes and then we're going to write the test so we're going to test after writing the code not we're not going to drive the code by via the test but this is a basic test it's a good starter we're going to start working on other things and as we add each route we'll come in here and write write some tests for it as well all right let's set up the database connection so you saw a little bit ago we installed mongodb as a dependency mongodb has built-in support for typescript and it's pretty good support um let's get it going so in my source directory i'm going to create a file called db.ts and this is where i'll set up the connection and i'm going to import need client from mongodb and then um actually no i'm not client it's uh something mongodb client client is what i need and then i'll need to create an instance of a client so i can say client equals a new client with my connection string and in this case i'm going to grab that from my environment variable so let's say the uri is pulled from our process.env now i've shown in some other tutorials basically how to how to build out like a config object and a config loader so that way we're not pulling directly from the dot env this is going to be our only environment variable so i'm going to do that here for now but i will link up here or or somewhere that describes the process of actually building out a like a config loader and a config object so you're not directly accessing it from there but regardless we're going to put it in our dot env and then i can i can pass it in here and typescript was complaining because technically from the dot env that could be undefined but i'm just going to default it actually i'll make the default value here like localhost to do api something like this okay so that gives us back a client and i'm going to export that and then we also get back um a database instance so i'll do it like this and one thing i found out today in my testing is that you actually don't have to do client.connect so what you typically see in a lot of code is basically you await this promise and then you spin up your express app or and then you start your tests what i found today is that you don't need to do this you actually can just start accessing your database and the first time you access a collection it will automatically connect it to the database so that's how we're going to do things here i will attempt to deploy this api and it's possible that when we deploy and when we're talking to a replica set instead of like a local db this might not work but it should work it should work for the most part um so that defines this and then i do want to create an a dot env file as well so i'm going to go ahead and throw uri in my env sample but i'm going to create a copy of this into a file called env and here's where i can put my actual uri and i think it actually will just be this i do believe i have mongodb running locally and it should attempt to connect to mongodb connected on my machine if you want to know how to do that you can look up db install on a mac and you can install it with brew there's essentially a brew tap brew update brew install you can run and you can get mongodb running locally you also could spin up a docker container or you could go to mongodb atlas get like a remote connection url and connect to that for me i'm just going to connect to my local mongodb okay now i'm going to define the collection so over here in my to do's model i'm going to import in that db instance so let's import db from go up to directories and then grab db like that and then um we will say to do's is db.collection of to do's so you pass in the collection name here but it supports uh uh generic so we can actually pass in the type so right now if i just do this to do's is a generic collection that will just return generic documents and documents are just objects that can have any keys but if i specifically type this and i say this is a collection of to do because remember we created that type earlier now when i make queries against it it's going to give me back typed information that have like these properties and when i do inserts it's going to make sure that i'm i'm specifying those properties let's take a quick stretch okay how do i want to do this i think i want to do instead of exporting default to do can i do export type yeah so i actually want this file to export two different things it's going to export the to do type itself and then also the collection type so that's great but now that i've done that i will need to update my route because before i was just importing the default export um so now i can specifically bring in uh to do and then i'm also going to bring in to do's and now let's query the database let's let's get it working so i'm going to make this an async function i'll say result is await to do's dot find i'm going to find all of them and then i can respond with results.2 array so that'll take the results um and turn it into an array yep and this is a good point so what it's complaining about right now is when i return these to do's from my database they're going to have an underscore id property because they're from the database so that makes sense as well i'm actually going to define another type in my model here let's call it to do with id and this is just going to use the with id helper from mongodb and you just pass a type in and that basically just adds the underscore id property to that type and so now i have this type that i can use in my route so over here i can now say that this should respond with an array of to do with id and we will pull that in here as well now this is complaining did i forget to use a weight i guess i did i didn't know i'd have to do that so i can say to do's is a weight dot two array and then we'll pass that in here it's good to go okay so i'm gonna run my tests the tests should fail because there's nothing in my database but it also could fail if it can't connect the database let's see uh invalid scheme expected connection stream yeah so right now um i didn't do the mongodb colon slash let's make sure i do that so uh in my env we'll put that there in my env sample we'll have that as well and then the default value over here will do that as well all right let's test it now nice so it's connecting to the database but right now the data we're getting back from the database is nothing because there's nothing in the database so we should update our tests and i think that that technically counts as a failing test we have a failing test we now need to update the code accordingly and basically when this when these tests run i'm going to run them against basically an empty collection so i'm going to do a before all where i drop the entire collection before the tests so that way it's more predictable as to what i'm testing against so before everything we can do to dos.drop like this so completely drop the collection and then now i should just expect that length to be zero um on the initial get this should be a passing test nice nice it failed why did it fail uh server error namespace not found i'm going to search the web for this error because i have never seen it before oh that's probably the case yeah so i'm trying to drop the collection that has nothing in it performing actions on collections cool cool cool that's actually perfectly fine i really just want to try catch this then and then swallow the error um yeah that's it that's it because basically the collection doesn't even exist there's nothing in it because it's brand it's a brand new database so that makes sense awesome um so we we have our initial to do collection it's completely typed we have our initial route that's also typed what i want to do now is i want to fix this error in our tests basically after all the tests are done running we need to disconnect from the database so the connection to the database is automatic but the the closing of that connection is not automatic so we need to we need to get that working so um we can do that in our jest config there's a few properties where we can specify a file to run after all of the the tests um it's like an env set up files after env i think this is the one um and then i'm going to create a file with this name in my source directory like this so this should run after all of the tests have run and what i want to do in here is say global.after all so after all the tests we want to close the database connection and this is where i can do db or not db client dot close and we'll await that and so this should run um after all the tests have completed which will close the database connection and get rid of this detect open handles thing josh all right we've got some passing tests um so milin is asking the question what is the difference between jest and supertest you can see that both of these are dev dependencies and then also because we're using typescript we actually have ts jest which lets us write our tests in typescript and then it can run b be run through jest but jest is our test runner and expectation library so when you look in one of these test files all of the things associated with running the tests and running the expectations all of those come from jest so before all describe it um expect right here all of that's coming from jest so all that's built into jest the things we're specifically using from supertest are is this so we wrap our express app in request that's super test and now these chained functions these are expectations and and methods we can call that come from supertest but you can see that i'm kind of combining uh super test expectations with jess expectations and you can really you can mix and match i mean technically you also like you don't have to do any expectations from super tests you could do them all with jest like i could do expect uh response dot uh status code to be 200 like these two are equivalent but it i think i just got so used to using super test and using its expectations but you could do it either way but the really the that's the only overlap between the two uh for the most part supertest is all about making the request to your express app and then just as about running the tests themselves okay at this point i'd like to do a little bit of refactoring before we move any further in creating this this api um i essentially want to take this handler and put it in its own file um and there are a lot of different ways you could go about this like if you've ever worked in an app before and you see that it's following a model view controller typically they might call that the controller file and then the controller has all of these functions inside of it i'm just going to call it handlers so i'm going to create a file called to dos.handlers.ts and then essentially all of the functions that i need to run in here are going to be specified in that in that file so i'm going to pull this out and this is going to be something like to do handlers dot find all and then over here i can define this function call it find all and then export it we need to bring in the request and response types from express and then we also need to bring in uh to do's so this file all of a sudden gets a little bit simpler and then over here is where we're doing the doing the hard work um and then i need to import uh to do handlers here so i can say import star as to do handlers from to do dot handlers like that and this should still work i've essentially just made this file a little bit more manageable because we're going to add a few other routes in here as well and then this is going to have our basically like our business logic okay in terms of what should i call this file or what should should should this be controller should it be handler should it what should i call it um often it's called controllers but typically when you call it a controllers you also have one more file that's really like the service and then the controller actually calls the service that's just more levels of abstraction we could do that i'm not going to do that i'm going to keep it simple and have this here but the reason you would want to do something like that is right now i'm essentially uh coupling my database access and my uh express request and response handling together so in the future if i wanted to move away from express and let's say use fastify it would take a lot of refactoring because right now i'm explicitly using the request and the response from express and i'm intermingling that with my with my database access whereas you could extract all of the database access stuff into a file called a service and then you could have your controller which then maps the request and the response to the service and so in the future if i ever wanted to change to some other framework i could do that um i'm not going to do that today we're just going to go this this level of abstraction but i will point you to this github repo it's called node.js best practices and one of the first things it talks about is layering your components keep the web layer within its boundaries and i'll show you this this animation that they have down here so by default you can see that this database access is just passing in the request which comes from express and now it's basically tightly coupling database access and and express and so dal stands for data access layer that's another concept you'll you'll typically see so instead of a service you might call it a data access layer but right now these functions are using rec from express and what they're saying is don't do that you can abstract it more so that they're not tightly coupled instead you can create this context object which picks off the things from the request that you need and then pass that into your data access layer and that basically decouples them so if in the future you want to swap one one or the other out you can do that like i said i'm not going to do that i'm going to intermingle them but that is a like an architecture pattern a design pattern that you could follow if you wanted to okay we've extracted it to this file the next thing i want to do is uh error handling um because right now if an error happens my express api would actually just hang up so let's do this let's make it so that my database doesn't actually connect so i'm going to connect to a server and uh it should it's the the request should fail now but let's see so i'm going to run this um in dev uh we'll go to localhost 5000 slash to do's and now it's just spinning and if i look in the console i don't see any errors and this is actually just is like an uncaught error i think at some point it's probably going to timeout because it can't connect to the database but if it does error out i might not even see that in the browser here and that's because i'm not catching that error so let's let's catch the error or actually do something with the error so i showed at the beginning that we have a generalized error handler so if you look in the app.ts file on line 28 we say app.use middlewares.errorhandler essentially you want you always want your error handler to be the last thing registered so all of your other routes are registered up here all of your middlewares the error handler is the very last thing and if any hint route handler or middleware calls the next function with an error object the error handler is what is going to get called so you can see that we get access to the error in here so essentially what i can do over here is i can wrap this in a try catch and then if it throws an error i can call next with the error and next is something that gets passed into the request handler here so i can pass in next which is of type next function you can import next function from express so next which is of type next function and then if there is an error call next with that error like that and so now if any shenanigans happen in my database access it throws an error and then now i call next with that error and then we have generalized error handling here it's going to put it into an object pull off the message add a stack trace instead of manually sending back an error response right here so this is really nice because all of our handlers can can use this and we can just do all of our generalized error handling in one spot so now that i have that let's see if it at least times out and maybe sends back an error so now if i request this i think eventually it will timeout but i don't i don't know what the timeout actually is um another way that we can just show this working is if we just throw an error something bad happened and this is just simulating if this code were actually to throw an error so now that we have that if i make a request to this page it just gets forwarded to the error handler we see the message and then we also see the stack trace you can also see in that middleware that i turn off the stack trace if we're in production that's a security precaution because you don't want to leak uh file names and and line numbers um in your error messages because uh attackers and hackers could use this as information uh when they're trying to uh attack your server so when we're in production we don't show a stack trace at all okay so that's generalized error handling let me revert back to our original database connection and now we should be getting back an empty array from the database which we are great um so at this point let's create a create to do handler so over here in my api to do's routes i'm going to want a new route which is going to be post on slash so post to api v1 to dos and i'll do to do handlers.create1 and so now i want to create one function in my handlers it's going to look like this so i'm just going to copy paste it but now we'll rename it to create one and now it is actually going to respond with a to do with id so after we create the to do it will respond with that we can also now describe the incoming request body so here i can say we don't have any request params but we do have the uh to do type here and if you look at request the first type argument is the params dictionary so that that's if you're if you have a url with params like id in it that's what that would be but in our case for this route there are no param so i did an empty object and then the the second argument is a response body so i actually did that wrong so the second argument should be to do with id and then the third argument is the request body so the incoming request to my server is going to look like this a to do with a content property and a done property now let's make it happen so the first thing we want to do is we want to validate the incoming body i'm going to do that in line here and then i'm going to show you a way of uh where we can basically define middlewares that do the validation for us so right now um i could do this i could say await uh to do dot parse singular to do dot parse did we import to do in my model oh oh oh i don't know when this happened but i need to export that too and so this is actually a feature of typescript if a single file exports two things with the same name one of them is a type and one of them is the actual thing you're allowed to do that so this is the schema validator and then this is the actual type i can call them the same thing and then now i can use them interchangeably um so now to do.parse is a thing cool because that's coming from zod and the way this works is it parses the thing that you're passing the object that you're passing into it and if there was no validation error it will give you back that as an object and then we can now use that object for our for our inserts so we could say uh wait to do's dot create insert insert 1 we specifically want to insert that in this gives us back a result or a insert result and this is really our validate result what is our insert result it is an insert one result of that given type so can i just res.json the insert result but now i would have to say that this is responding with an insert one result of type to do like that then i can change the return type here as well uh insert one result comes from mongodb and so now that responds like that this should be all we need let's write a test for it so if we go over to our tests i now want to describe creating a to-do so i'm going to copy paste this so i'll say when i make a post request against api v1 to do's it should respond with an error if the to do is invalid so i now want to make a post request to api v1 to dos and then the way you actually send data is if you you just do dot send and so this is the request body that i'm actually sending to the back end in this case let's try sending uh an object with the content uh completely empty because this should fail validation because we we said that content should have a minimum length of one and so here we want to let's just do this uh to make it fail for we want it to fail and i'm right now this should fail because it should return a 500 status code not a a 200 status code and actually we can also work on the status code we return because it's technically a validation error which wouldn't be a 500 error but we'll work up that next so i'm just going to say make sure the body has a property message [Music] and expecting 200. so now if i run this test it should fail because of the status code that's what i'm expecting so right now it's responding with uh 500 status code uh and i was expecting it to be a 200 status code um so let's handle that um really what i want is if this error is a zod error then i want to set this the status to be um like for something because 400 i believe is a client error there's a there's a status code for when the body is invalid we'll find that i'll have to figure out how to check if it's a zod error here we would do res dot status of 400 bad requests something like that i'm going to go look at the zod documentation to see if they have any info on determining that might be it 422 unprocessable entity i think that's the one i want like you sent me something and it is not processable oh do i just do oh i i did this wrong so error instance of zod error cool so if there was a validation error set the status code to 422 that's what i want and then now in the test i'll validate that the status was 422 and i get a a message property um in in the response um i'm gonna log that just so we see what it is but it should be something about the the content property not being long enough so let's run the test nice test pass and we can see we get this error response so too small string path content string must contain at least one character cool um i'm not going to worry about formatting these error messages but what's if i were to format them i would probably do it over here so in the middleware i could check to see like um if the error is an instance of a zod error i'd have to make sure i import that in yep um is an instance of that error then error message is something like error dot uh issues for each one grab each issue and then grab each message something like that i'm not gonna do that here i think the the zot errors are enough and whenever we build a front end for this we technically can parse these errors on the front end and then show this information on the front end as well okay so i'm going to leave those errors the same we have passing tests i'm going to remove this console log cool so i'm going to write another test here it responds with an inserted object and so now we'll make the post request we'll do learn type script and then we'll say done is false right now i'll expect a 500 status code so that it fails but eventually it will have a content property and a done property and it should actually also have an id property because it just got inserted into the database like this so let's run the tests the error i'm expecting is the status code is wrong yeah so right now it's responding with 200 but we're expecting 500. so now if we expect 200 it should have an id property um it should have content and it should have done and actually i am just going to log so we can we can visually see see it but um i guess we can we can do some more expectation here though we could expect uh response response.body.content2b learn typescript like that um cool let's run our test now cool okay so uh this this is an issue so the thing we were getting back from the insert is not what i expected um this is actually what mongodb is giving us back yeah and when mongodb inserts the the document it will put the underscore id property on it so we actually need to use this inserted id thing so let's handle that we can say if insert result dot yeah so i thought i thought we had the the full thing but we don't so if it was not acknowledged we need to throw an error uh something like error inserting to do that's it that's all we'll say the error insert so if it wasn't acknowledged we throw that error that will get caught it's not a zod error so then it's actually a 500 uh error which makes sense because then it's more like a server-side error like something like the database went wrong in the database but now i actually want to return the validated result along with the id which is the insert result dot inserted id like that and now we want the to do with id type as the response that's what that should be uh we because basically the this mongodb driver whenever we do an insert it doesn't give us back the thing that we inserted it just gives us back whether or not it worked and it gives us back the id um and so i can respond with an object that has the id and then also the data that was sent to the server and validated um because that's what most people would expect from the api you get back what was created along with with the id so that that should fix that great you got it uh so this was mentioned by nadoshi should we have a 201 status code and i think that makes sense so i looked it up and 201 is the status code for something was created and in this case we created the thing in the database so it does make sense to set that status code to 201 and then now we should expect status code 201 cool and i think but josie has a good point let's see how i wrote this good call i actually want to call parse async yeah because parse async does try to do things behind the scenes and and then resolves a promise before this was actually blocking um so good call we should make that parse async i mean it is i i get it eddie because isn't it nice to just be able to run npm tests and be like yeah test pass i guess my code's working instead of like instead of having to like click through multiple things because like the other way we would test this is through um postband or or insomnia we admit we would manually have to make the http requests verify the response and then every time we change something we'd want to do that same thing but now we're basically codifying that process into code which is nice so now i would like to refactor this and pull out a middleware function so um this validating of the incoming request body i'm gonna have to do that in at least two different places right i'm gonna have to do that when i'm creating a to-do and then also whenever i'm updating it to do because the request body is going to look the same in both cases so what i'm going to do now is i'm going to write like a generalized way of defining how to validate the incoming request and then we're going to move this into that validator so first let's take a look at to dos.routes and basically what i want to be able to do is i want to pass in something like validate request like this and if you're not familiar in in express essentially any one of these functions you pass in is a middleware function and and basically we can we can write code here that will look at the incoming request body if it's good call next and allow this handler to execute but if it's not good throw an error and then this handler will never execute so i want this magical validate request function to exist and i want to be able to pass some options into it and so in this scenario i want validate request to validate the body so i'm going to pass in an object where the body is to do so this is our our to do zod schema right and uh now as the request comes through it should do that parse function if it fails through an error otherwise uh set the request body to be what the parsed value and then call next um and uh for other routes i also want to be able to parse like the params um like the prims in the url like uh id or different things like that and then in some scenarios i might want to parse and validate the query string as well for now we're just going to handle the body so i want a function called validate request that takes in the validator validators um every express middleware needs to be a function that takes in request response and next right so what we need to do from this is we need to return a function that takes in a request a res and a next like this we can type these from express so we can call this request from express response from express and then also the next function like that great um why is this complaining yeah because they're never used and then essentially what i want to do is a little try catch right here so i want to say something like validators dot body and i want to pass in the request.body and i probably want to do dot parts because i'm going to expect these things to be uh like a zod validator so i probably want parse async i want to get back the result and await it and really i just want to say rec.body equals that like that and then also this would be an async function cool and if all was good we can just call the next function and that means it'll go on to the next thing and if there was an error we can call the next function with an error we can do what we did earlier and basically we can actually pull this out because now we're doing the parsing in the validator and so over here we can say if that error was a zod error then set the response status to 422 uh which it should have been anyways um all right now let's get this working so we don't have this type i want to define an interface called validators or let's call this request validator um and it will have a body property which is of type zod object any zod object that and then this will be optional because for for some validators i don't want to specify the body sometimes i only want to validate the params or only validate the query string so we'll also have uh params and a query like this and then the thing that we're passing in here is of type uh request validators yeah and um we can basically just wrap this in an if statement so if we defined a a validator for the body then um try to parse it with zod and then take that result and put it back onto the body um and then we can do the same thing with uh the rec.params if i say rec.params rec.params equals validators.params parse it and then same thing with the query string so rec.query will be validators.query parse async with rec.query like that so now this is a general purpose error handler basically we pass it an object that has a validator for any one of these things and then it will look at the incoming request and validate those things but if there was an error it's going to catch it and immediately halt if there wasn't an error then it will call next and go on to the next error handling function and so now we're doing that right here where we're essentially validating the incoming request body making sure that it is of type to do and so now let's actually move this into we'll move validate requests into our middlewares file so over here we have middlewares now let's define a function called that so export validate request like that we'll move this interface into the interfaces folder so over here we'll call this request validators dot ts and we'll export default uh pull these in i don't want that i want to pull this in from zod like that cool so we have request validator um in here now instead of doing this we want to import in from the middleware so import star as middleware well we could import star let's also let's just import the function so we'll import validate request from middlewares we don't need these types anymore and now the code is working over here so i just basically took the code i wrote right there um and then moved the function over here to middlewares this needs to pull in that interface now from interfaces and we should be good to go but what we can do now is now in this to do handlers.create1 we can remove the validation because now we know that the code will never make it here unless it was validated and here i can just pass in rec.body direct.body and it's actually safe to do this insert with rec.body because rec.body got overwritten by our middleware so our middleware literally replaced the incoming body with the validated body so over here when we're passing that into our database that's actually the validated version of it and again it's validated because it has to run through this function first all right um let's see if we did it correctly we're going to run our tests at this point we just did a refactor we didn't add any new features so we would expect that all of the tests still pass cool um looks like i have a console log laying around somewhere let's get rid of that let's get rid of this but the tests do pass all right next up let's create a get one single to do route so we already have that route that gets all of them we want to create a route that gets one by id like this and we'll say find one now in this case we can add a request validator that basically just makes sure that id is a string so we can do a similar thing here so when you request api v1 to do slash sum id i want to validate that the params are of type id params i'll have to create this interface like params with id something like this and so then i can have an interface that just defines an object with a property id so let's create that over here so params with id is an interface that has an id which is of type string strong and actually i should i should define this with zod so instead of defining this as an interface i define it with zod so that way i can i can pass it as the validator so let's do this we will import zod like so and then we'll say export const params with id equals z dot object and then that object has an id which is a string i mean really it's a it's actually a mongodb object id um so i technically could validate that for now we're just going to make sure that it's a string that is um not empty and then i also want the type so params with id is z dot infer the type of params with id like that um so now i have this validator and interface um and we can use it here so we'll import it in from interfaces and now whenever a request comes into this route we're going to validate that the id here is a string it isn't empty though i do want to look into validate object id because that would probably be better validation we can basically prevent the dblookup by making sure that this is a valid object id cool and then now we need the find one handler so over in this file we're going to export a function an async function called find one we get the wreck the res and next do our little try catch here and then we'll type these things so this is a request the params we do know we now have a type which is params with id um requests takes in the params dictionary the response body so find one should respond with a a to do with id and then the request body should be empty so there should be nothing in the request body and then the response should be to do with id like that and then this is the next function cool so now we can do the work so we'll say to do's which is our collection we want to find one we need to pass in the query so i want to find one where the id is rec.param and the issue here is this is a string it wants id to be an object id so i should be able to pull in object id from mongodb there so that imported object id and then i can say new object id with rec.params.id like that result is a to do with id so i should just be able to do res.json for the result however result is possibly null meaning they didn't find it in the database and if we didn't find it in the database we'll set the status code to 404 and say to do not found so if we didn't find it we can do res.status 404 and then we'll do [Music] we'll throw a new error and we'll say uh to do with id uh rec.params.id not found like that um should be good uh the one issue here is like i said we should be validating that the params id is of type object id i'm not quite sure how we would do that with zod just yet so we'll figure that out after we we test this route so now let's write a test and what i'm going to do is i'm going to write a test that uses the result of this previous test so um this test is the one that creates an object so let's say i define a variable out here called id like this and then i should get back from here response stop body dot underscore id like that and so now i can write a test that makes a request for that specific that specific to do so get to do's slash id responds with a single to do so now i want to make a get request on api v1 to do's slash that specific id that we got back from the creation and then we can do some similar expectations here so expect it to have all of these properties and expect the content to be learn typescript i guess we can also expect the id to be that specific id that we requested cool so again i'm writing a test that's probably going to pass on the first try um we should also write a test for if the id was not found yeah so that passed um let's say we do a another test responds with a not found error so if we try to request [Applause] this i want to expect a 404 however we're probably going to get a 500 status code because this isn't a valid object id hmm and then now i actually will call don right here um this doesn't want to return anything like that um this will be where we need to fix that object id validation so i gave it 200 uh did i maybe i didn't save the file let's run the test one more time yeah there we go and that's what i was expecting so uh the 500 status error is when it gets into this handler it's trying to create an object id with something that is not an object id so i now want to figure out how can we validate an object id here with zod so let's figure that out refine yeah i think that's what we want and then we'll actually turn it into an object id [Music] and so refine gives us back the value and we want to do a new object id with that value and then if that fails then it's a well i guess really we kind of want to wrap this in a try catch how does refine work refinement functions should not throw instead they should return a falsie value to signal failure ah okay so in this case we will return false that's what we want to do so if it fails to parse um throw false okay and then now that should be a zod error and not a uh 500 error yeah 422 unprocessable entity um so uh our to-do test should now expect that um you you requested some like in it basically an invalid um invalid id and if we look at the um we can do this then if we log the response body response body message we should see a zod error that says like um not a valid it wouldn't say valid object id but it would say i don't know what i would say invalid input cool um but we can update that let's do um i think you can also pass in it says you can pass in a message as the second argument an object with a message property yeah invalid object id and now we should see the message invalid object id nice nice um so this is testing something different this is responds with an invalid object id error like that and then let me just steal an id that won't be reused like this and then we can test for the not found so it responds with a uh not found error and then we'll request this one the cool thing about object ids are that they are they're actually time based so embedded in this is a way to get the time that the document was created which also means that this won't be repeated um in any new ones we create because they're getting created in the future so we'll expect this to be a 404 so this should be good yep and then um i mean if we do to do slash two or to do three we should see that same uh four two two response error because the number two is not a valid object id yeah uh right here to do slash three i'll go back to like that weird one though all right next up we are going to create a update1 endpoint so right now we can find them all we can get one we can create one now we want to update one so that is going to look like this so we'll say router dot put so if you make a put request to this specific id we're going to validate the id just like we did with a git but now we're also going to validate the body and we're going to validate that the body is of type to do and then we're going to call update one so you can see how our earlier refactorings are helping us now where essentially now i can use this validation function to make sure that the incoming id is valid and the incoming body is valid um and i'm specifically implementing a put request which is replace the thing at this specific id with the request body you could also implement patch patches usually you allow the client to send a partial object so only send the content property or only send the done property we could implement that as well but for now i'm just going to implement put which replaces the entire thing at that given id and so now we need to implement the update1 function so we'll go to our handlers we'll say async we'll export async function um i'm going to copy this and then update it accordingly so the incoming request will have an id in the param we are going to respond with a to-do that has an id after you've updated it will respond with it and then um the request body needs to be a to do because the the incoming request is a to do and then we are responding with it to do a to-do with id great so we've got our try catch call next with the error um let's go ahead and write a failing test so i i haven't been doing that basically we've been writing it and then just testing that it works let's write a failing test first um so we want um we're going to describe a put so if we make a put request to some specific id um we should handle both actually handle both of these not found scenarios um so if you're trying to make an update to something in an invalid object id um that should result in the same error so now we're going to test with a put and same thing here so i know i've just copy tested copy pasted the test but the one difference is now we're making a put request versus a get request so there's that and then if we make an actual uh put request with an update let's do it in this one um so when we make a put request to this endpoint it should re if it's successful it should respond with a single to do and so we're going to make an update against the one that we created earlier so that same id we should expect can someone tell me what's the status code for when something is updated or replaced uh for now we'll just do 200 and we want to send an actual object so we want to send what do we want to update this to in this case we're going to update the content to be learn typescript but we're going to set done now to be true because done was false before 201 created well if it's updated is it still created regardless so we'll expect it to have the id have a content property and then we should expect done to be false or to be true 204 resource updated successfully good to know so we'll do status code 204 so when i make a put request to my api at this given id and i'm sending the whole object and i'm setting done to true it should respond with the 204 and the done property should now be true let's run our tests we should see a failing test what's nice is we did get two extra passing tests out of that the two puts here because we tested those the exact same way that we tested the the gets for invalid object id and not found expected 404 not found got 422 unprocessable entity unprocessable 204 is no content all right well we'll have to we'll have to update this i'll look that up in a second i want to make sure that this is failing for the right reason so put api v12 id that is gonna run here and i guess right now we're not doing anything so it should just time out i think that's the main issue it's just timing out yeah i mean according to this wikipedia article i should just go for status code 200. so that's that's what i'm going to do uh in the test here so this should be status code 200. okay first off let's get the uh so this one is technically working because the uh params validator is already we already coded that earlier and we could test that here now uh we want to do this oh i see that's what and okay so i saw in the errors we got a 422 unprocessable entity that's because we didn't specify a body in the not found one so technically if i were trying to do a put i also would be sending some data along with that like uh done false or done true or something like that done true content learn typescript so technically i would be sending that this would get validated and then it would be a 404 not found but right now this is just going to time out because my handler isn't doing anything so let's handle the not found scenario so we'll get back some result and we're going to await our to do's collection dot we're going to choose find one and update so this will search the db for the thing if it finds it it'll update it if it doesn't we'll know what happened based on the response so we need a filter we're going to filter it the same way we did earlier we're searching for a specific document with this id and then the updates that we want to make are set so we want to set the properties from rec.body so rec.body is the thing that we validated already uh the request body and that's what we're going to set those properties to um and then one last thing we need to specify here is what we want to return and in this case we want to return after meaning return after the update is complete so this will find it update it and then we get back that updated document so here we can say result dot value so value will be the updated thing if it exists so if it doesn't exist then we're going to throw an error and we have we can reuse this error basically uh we didn't we didn't find it so and we couldn't update it so it wasn't found but if the result does have a value then that's it that's all basically um that is the updated value and that's what we'll respond with so we should be good here no type errors let's do some tests see what we got good to go great all right so the last handler that we need is the ability to delete one specific uh to do and so it's gonna it's gonna look similar to the the put um except the method we're expecting is delete and we're only gonna validate the request params so when you're making a delete request the body is completely empty so we don't need to validate the body we just validate that the incoming id is a valid object id and then now we can define this delete one function so let's do it we'll have our async function delete one i'm just going to go ahead and copy this and then we'll update it accordingly so the incoming request does have an id so that's correct the response body is actually going to be i think just an empty body and the incoming request body is also empty so i'll specify it like that and then the response responds with nothing so we'll do a try catch if there was an error we'll call next with it um and why is this all complaining oh i need to export it great um at this point i'm gonna write my tests first um and make sure we get some failing tests and then we'll write the code to make it pass so if we go over to our to do's tests i actually am just going to copy this put because it's going to be very similar but this is now testing for delete um and then we want to test all of these same things like does it validate the id if i make the request here does it respond with the 404 if it wasn't found though i do have to decide what's the proper way to respond to a delete like do i always just respond 200 we'll see we'll see i think for now if it wasn't found respond with 404 um and then responds with a 204 status code so if i now do a delete at that given id we're not sending anything there's nothing in the request body we're just making the delete request we're expecting that we should get back a 204 meaning that it was deleted and that's the standard rest response for for deletion you don't respond with a body you just respond with an empty body and a 204 status code so what we also want is this cool uh we'll run our tests and uh it's gonna it's gonna time out because right now that air that that handler that we wrote uh has the try but then nothing happens in the try so both of those requests timed out the 404 and then the actual deletion so now let's make those tests pass so we'll have our result and we'll say await to do's dot delete one i guess if i do find one and delete it might be easier to to uh make sure that it exists let's do find one and delete because that way we can write it in the in the similar way that we did find one in update um honestly i'm gonna copy these options and here we'll pass them in but in this case i only want that that why is this complaining oh um [Music] so find one and delete takes in a filter a modify result in this case we're not trying to modify anything so empty and then we want want that thing back so say if result dot what does result give us actually do we get this same option here i guess in this scenario um we don't even care there will there won't be an option for return document after that makes sense here because um we're not updating anything we're just finding it and i believe i can just pass it in like that cool so just find it delete it if you found it now if result.value that means we found it and also means that it got deleted so if there was no result value we can do the similar thing we did before where we set it to a 404 and send back the error and then lastly i'm just going to do res.send or res.status 204 dot end so don't send back a reque a response body um only a 204 status code that i got deleted and end it and if the test passed great but i want to write one more test that make sure after deletion if we make a get request for that same item then it responds with 404 because it should be deleted at that point um expected content type header field that's that's my fault so uh in the in the test um where i left this expect content type in this case there is no content type we don't even need an accept because basically we want to delete it and then just expect that we get that 204 status code so let's do this great fully passing test awesome but like i said i want to write one more test that makes sure that after deleting we if we try to request that same to do we should get back a 404 not found so i want this one so delete it and then if we make a get requests we'll have to pass in that id we should expect a 404 and this id needs to parameterize to be the one that we created earlier okay moment of truth so after deleting we have to make sure that we get back a 404 and it doesn't exist anymore hey fully passing test awesome that's it i think we're done we did it we did it in three hours we built a crud app with tests with models of with uh typescript validations we have route validations it's good stuff all right a quick correction that was brought to my attention by alka and they mentioned well why are you awaiting fine you see those three dots right there vs code is saying it has no effect which means find doesn't actually return a promise so i made a mistake here and if you look in the documentation for mongodb when you say find that actually gives you back a cursor and that makes sense when you're dealing with a database with a whole lot of data because if you were to just do a select all or a query all that could be a whole lot you could run out of memory it could take a really long time which is why they give you back a cursor and then you can go like one page at a time in our situation i know that there's not going to be that much data because we're just dealing with to do's here so i actually could change this to be to do's dot find dot to array and call that to do's here so if i make that update now it makes sense because this is the cursor and then i'm saying take all of that turn it into an array which two array returns a promise and then we get back the data there so a quick aside we should be doing it this way thank you for watching this video um hopefully you learned something and if you want to take a look at the code if you go to the repo in the description there is a branch called solutions and if you go over to the solutions branch this has the api that i just implemented you can look at all of the code here all of the all the things we wrote you can take a look at it there in a future video we will do more refactoring so we'll talk about things like data access layer and controllers and services and in a future video we'll also build a client for this api so we're going to build a client with react in typescript and we're going to do a little bit of refactoring so that way we can reuse some of the types that we defined here and then we're also going to build a client with view and typescript so again hope you enjoyed this we'll see in the next one [Music] you
Info
Channel: Coding Garden
Views: 42,076
Rating: undefined out of 5
Keywords: javascript, coding, programming, node.js, tutorial, learning, debugging, web development, web, full stack, live stream, live coding, how to, app, learn typescript, what is typescript, typescript generics, typescript, is javascript typescript, whats the difference between javascript and typescript, inferred types, type annotations, express, zod, mongodb, validations, CRUD, API, Web API, Rest API
Id: vDLE8hqzA8I
Channel Id: undefined
Length: 98min 56sec (5936 seconds)
Published: Sat Aug 27 2022
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.