Info Express.js is a really neat and highly adopted framework for Node.js that makes it trivial to build an API. It's by far the most popular one due to it being created near Node.js' time of creation and how much support the community has poured into it.
Express is synomous with:
Django for Python, Sinatra for Ruby, Spring for Java, and Gin for Go Lang**
Why would I even need express?
Why would I need express ? I can do all these stuffs in node anyway !
The above works, It doesn't do much functionally, but it works. A client can issue a
GET
request to the server at/
and get back some JSON. Any other request will yield a different message and a404
status code.
Why this breaks down
When building something trivial like our example, then not using a framework is fine. Maybe even preferred. But you’ll have to start creating your abstractions as soon as you build anything.
Why create your own when a framework is just that: Abstractions based on some opinions that benefit from having community support.
Little bits
Every API ever shares a common make up. Doesn’t matter the language or the environment. What makes them different is how each one does it and what they build on top if.
Server
An API is a Server.
This may seem obvious, but really needs to be understood.
- 🔥 A server is an app that has no visual representation and is always running. Usually connected to a network and shared among many clients (UIs, web apps, mobile apps, other servers, etc). Servers usually sit in front of a DB and facilitate access to that DB.
- 🔥 There are small exceptions here, and that would be Serverless APIs. Big difference with serverless APIs is they are not always on like a traditional server. Servers must operate on a port, a virtual place on a computers operating system where network connections start and end.
- 🔥 Ports help computers sort out their network traffic. A server must also have an IP address, a unique location used to locate a server on a network, like the internet. An IP address helps traffic go to and from a specific device whereas a port allows targeting of specific services or apps on a device. An example would look like this:
127.0.0.1:5000
. Where5000
is the port and the rest is the IP address.
Route
A route is a unique combination of a URL path and a HTTP Method. Routes are used to locate certain resources or trigger certain actions on an API.
HTTP Methods or Verbs are constants that are used by API developers and HTTP to help determine intent of an API call. There are many methods, but the common ones are:
- 🔥
GET
- ➡️ used to get information from an API
- 🔥
POST
- ➡️ used to mutate or create new information on an API. Usually has data sent along with the request.
- 🔥
PUT
- ➡️ used to replace existing information on an API. Usually has data sent along with the request.
- 🔥
PATCH
- ➡️ used to update existing information on an API. Usually has data sent along with the request.
- 🔥
DELETE
- ➡️ used to remove existing information on an API.
- 🔥
OPTIONS
- ➡️ used with CORS by browsers to check to see if the client is able to actually communicate with an API
Here are some examples of routes:
GET /api/user/1
POST /food
Engineers can design these routes and what the routes actually do however they see fit. To standardize this, there are different approaches to designing these routes. The most popular is REST. (There are others like grpc, graphql, and protobuff.)
Route Handlers
A route handler is a function that executes when a certain route is triggered from an incoming request. Depending on the API design and intent of the request, the handler will interact with the database. Looking at our route examples above:
- 🔖
GET /api/user/1
- ➡️ if this API was a REST API, the route handler would query the database for a user with the ID of 1.
- 🔖
POST /food
- ➡️ if this API was a REST API, the route handler would create a new food in the database, using the data sent along with the request.
ORM
You need an ORM
When it comes to choosing a DB for your API, there are many variables at play. For the most part, you’ll end up with a Relational (SQL) DB or a NoSql DB (Document Store). We’re not going to get into what is the “best” DB because that’s impossible to answer and changes as your product’s needs change.
However, no matter the DB, how you interact with the DB matters. What good is the perfect DB that is painful to interact with. Enter, and ORM. Object-Relational Mapper (ORM) is a term used to describe a technique that allows you to interact with a DB using an object-oriented approach. When most people say ORM, they’re actually talking about an ORM library, which is really just and SDK for your DB. For example, without and ORM, you can only interact with a SQL DB using SQL.
To insert a customer, you would write an SQL Query that looks like this :
This is probably nothing if you know how to use SQL, but with an ORM it would look something like this :
What is Prisma
Prisma is a DB agnostic, type safe ORM. It supports most DBs out there. It not only has an SDK for doing basic and advanced querying of a DB, but also handles schemas, migrations, seeding, and sophisticated writes. It’s slowly but surely becoming the ORM of choice for Node.js projects.
Setup prisma
Psql
We’ll be using PSQL as a DB in this course. You won’t have to install anything as we’ll be using a hosting and managed DB from Render. Go there, create an account, and then create a FREE psql DB.
Installing Prisma
Prisma works best when you’re using a TypeScript. So in addition to installing Prisma, we’re going to convert our app to TypeScript. Don’t worry if you don’t know TypeScript. We won’t be doing all the fancy typed stuff in this course. We just want that sweet autocomplete for our DB interactions through Prisma. Trust me, it’s magical ✨. On to the installing!
Then create a tsconfig.json
file which is the config file for TypeScript. Add this to that file:
Next, we'll initialize Prisma
Why are we suing npx ?
Sometimes you install a package, and it’s not accessible in the terminal, and you would like to refer to it but then can’t unless you specify the full file !
This command will do a few things:
- 💡 Create a prisma folder
- 💡 Create a schema file in that folder Next, we’ll learn how to design and create some models in our schema
Prisma Syntax
Prisma has an easy-to-understand syntax for creating models. It’s based on the GraphQL language which is based on JSON. So you’ll feel right at home. I highly recommend installing the Prisma VS Code plugin. It lints and cleans up your schema file.
Now, onto the models. Let's look at an example model.
Most of this is self-explanatory, but check out the comments in the code to learn a bit more context. This isn't a prisma course, so we're going to keep moving along on our API. The rest of the modeling looks very much like this.
User
Above is our User schema
Product
Product schema
Here we have a Product schema. For the change log app, the user might have many products they want to update. So we need a place to store multiple updates. So
products
belong to aUser
.
Update
Products can have updates.
So products belong to updates. Updates have many fields, one is called status. Because status is a finite set of options, we created an ENUM to represent our status. Think of an enum value types as “one-of-these”. So the value must be one of the values in the ENUM instead of being any other random string.
Update Points
And finally, update points are the bullets points on an update. They belong to an update, which belongs to a product, which belongs to a user.
Migrations
Since this is our first time interacting with the DB, we need to run our initial migration to get the DB and our schema in sync. We’ll continue to run migrations as we make schema changes to ensure the schema and any data in the DB stay in sync. Before we run a migration, we need to install the prisma client, which is the SDK we’ll use in our code to interact with the DB. This client is type-safe and based on of our schema. It’s actually an NPM package that gets generated on demand to adjust to your schema! Pretty cool.
Next, lets migrate the DB. Make sure you added your DB connection string to the .env
file as DATABASE_URL
. You can find the connection string on render. Be sure to use the external one. Now to run the migration:
This will migrate the DB over to use our schema and then generate the new client for us. This client will be used in our code and is now type-checked against our schema.
Thinking of Routes
Until some other need presents itself, we want to create a route for every CRUD action for every resource. So, in the case of a Product, we want to create:
- 🔥
GET product/:id
- ➡️ get a product by a given ID
- 🔥
GET product
- ➡️ get all the products (for an authenticated user)
- 🔥
POST product
- ➡️ create a new product
- 🔥
PUT product/:id
- ➡️ update or replace a product that matches a given ID
- 🔥
DELETE product/:id
- ➡️ delete a product by a give ID
This is how REST looks. However, when developing an API that's consumed only by a client that you and your team also created, using something like REST is probably redundant and tedious. There's nothing stopping you from just creating an API route to get all the data for every page, or every component, or whatever makes sense for your application. Something like REST is great for external APIs so external developers can onboard more quickly because they know what to expect vs. having to learn some custom API design.
Create our routes
Create a new file, src/router.ts
and work in there.
There are a few things going on here. First we created a new router using Express. This gives us more flexibility around configuring a set of routes vs. the whole API. You can create as many routers as you’d like with Express and mount them back to the main Express app on the appropriate paths.
We then created all the routes for the DB resources we want to interact with. User is noticiably missing. This is because User will have a special set of routes because of the importantance of that resource. For the handlers, we adding placeholder functions for now. If you try to make an API call, your API will get back a 404
status code and some HTML (default 404 response from Express). That’s because we didn’t mount this router back to the main Express app. So it’s just floating and not actually attached to our API. Let’s do that next:
head over to src/server.ts
:
Import the router from the other file and remove any current route declerations in server.ts
. We then use something new here: app.use()
, this allows you to apply a router or middleware (we will learn about middleware later) to the entire API, or in our case, to anything using the path /api
. So a route we create in the router like GET /product
, is now actually GET /api/product
because the router is mounted on the /api
path.
You should now be able to hit your API and not get a 404, but, it still won’t work. What’s happening now is your API is hanging, which just means it never responded back to the request and there is no more code to execute. The client will eventually timeout and close the connection. This happens because we never send a response in any of the handler functions we created. We’ll do that soon, but for now, lets talk about middleware.
Middlewares
Middleware are functions that run right before your handlers run.
They can do things like:
- 💡 Augment the request
- 💡 Log
- 💡 Handle errors,
- 💡 Authenticate,
- 💡 authorization
- 💡 ** and pretty much anything else…**
They look exactly like a handler with one difference:
Because you can have a list of middleware, there needs to be a mechanism to move into the next middlware function when work is done in the current middleware. It looks like this:
This
next
function is exactly what it sounds like. It tells Express that we’re done in the middleware, and it’s safe to proceed to whatever is next (more middleware or a handler).
To apply the middleware to a route, you can do this:
Middleware will run in the order in which you passed them as arguments.
Examples of middleware
Custom middleware
You can easily create your custom middleware, even ones that take in custom parameters, since at the end of the day, they are just functions!
Authentication
We don't want just anyone using our API.
Our DB is multi-tenant, so we need to identify what user is making the request so we can scope their queries and writes to the user.
We don’t want one user having access to another user’s data.
To ensure all of this, we’re going to protect our API.
Tokens are a great approach for this. Things like API Keys and JWT’s are good examples of tokens. You could also use Sessions.
Creating a JWT
Lets create a function that create’s JWTs for when a new user signups up or current one signs in. Users will need to send the JWT on every single request to get access to the API. Our API never stores a JWT, its stored client side.
We need to install a few things:
Create a new file
src/modules/auth
and add this:This function will take a user and create a JWT from the user’s id and username. This is helpful for later when we check for a JWT, We then will know what user is making the request. To do that check, we’ll create custom middleware.
This middleware functions checks for a JWT on the Authorization
header of a request. It then attaches the user to the request object before moving on. If anything fails, the user is sent a 401.
We need to update our .env
file to have a JWT_SECRET
.
You don't want this secret in your code because it's needed to sign and verify tokens. You can place whatever value you want. Then we need to load in the env file into our environment. Inside
src/index.ts
:
This will load in our env vars into the process.
Protecting our endpoint with the middleware
Lastly, we need to add our middleware onto our API router to protect it, so inside of
src/server.ts
, import protect and add it to the chain:
Now, any API call to anthing /api will need to have a JWT. Next we’ll create some routes and handlers to create users that are issued JWTs.
Password Encryption
We know from our schema that a user needs a unique username and password. Let’s create a handler to create a user.
Before we can do that, we'll create some helper functions to hash and compare a user's password so we're not storing it in plain text.
The reason we do this is because, if someone access our database, we don’t want them have direct access to our user data.
- 💡
comparePasswords
- ✅ compare a plain text password and hashed password to see if they’re the same.
- 💡
hashPassword
- ✅ hashes a password.
Now, let's create that handler
First thing here is the prisma import. I’m creating module that exports a Prisma client so we don’t have to keep creating a new client every time we need it.
There isn’t anything special going on here other than creating a new user then using that user to create a JWT and sending that token back as a response.
Next, we need to allow a user to sign in.
Using the provided username, we search for a matching user. We’ll get more into how to query with Prisma soon. Then we compare passwords. If it’s a match, we create a JWT and send it back.
Now we need to create some routes and add these handlers. We can do this in src/server.ts
Route & Error Handlers.
Never trust the user!
Words to live by when working with user input. Especially more true for an API that is responsible for holding the weight of every client.
The last thing you want is for a user's input choice to crash your entire server,`You will lose the respect of your senior.
We want to get ahead of that and validate all incoming data for our API.
Now that you have understood why not using a library is a terrible idea, how does validation look like in express?
There are many libraries that can get the job done, but in express, the most used and popular one is called express validator.
For any route, you want to add input validations:
We can use the provided middleware to create new validations against any user input. This includes the body, headers, cookies, params, and query string. In this example, we’re validating that the request includes a name
field on the body.
Let’s apply input validations to all our put
and post
requests.
Tips and tricks
Nodemon
If you want your server to auto-restart when there is a change, you can use nodemon.
Nodemon Setup Instructions
Install nodemon
Nodemon is installed as a development dependency because, once the application is deployed, the web host will be responsible for starting the server.
Once nodemon is installed, update the
dev
script in your package.json file
Prisma studio
Do you want to visually see your data ? no problem Prisma studio got you.