API Design in Node.js, v4
Table of Contents
IntroductionScott Moss introduces the course and walks through the course website. The API project will be built from scratch and the final code can be found in the GitHub repo linked below.
Tooling OverviewScott walks through the tools and technologies used throughout the course. The application will be a product change log API and it will be built with Node.js, Express, Prisma, PostgreSQL, and hosted on Render.com
API Basics in Node.js
Creating an HTTP ServerScott creates a basic HTTP server using the built-in http Node module. As requests are made to the server, conditional logic determines how the server should respond.
Anatomy of an APIScott explains the different components of an API including HTTP methods, route handlers, and how IP addresses and ports work. Most of the work of an API happens inside the route handler. These handlers can contain logic for determining how to handle requests, authentication, and limit the number of requests to protect the server.
Creating a Server with ExpressScott refactors the code to use Express instead of the built-in http module. Express is installed with NPM and provides helper methods for defining routes. Depending on the functionality of the API, route handlers can return anything from basic text or JSON, to HTML files.
Object Relational Mapper (ORM)Scott introduces ORMs, or Object Relational Mappers. ORMs provide an API for conducting database operations and eliminate the need to write SQL statements inside the application. A question about using JSON in Postgres vs. Mongo is also answered in this lesson.
Prisma & Render SetupScott explains how Prisma, a database-agnostic ORM, will be used in the application. A PostgreSQL database is created on Render.com. Prisma communicates between the application and Render.com using the external database URL located in the Render.com connection settings.
Prisma OverviewScott installs Prisma and runs the CLI tool to generate the required files for the project. The prisma.schema file contains all the schemas for the data models. The database connection string is added to the .env file.
Designing a SchemaScott creates a schema for the User model. Each user has an id, username, password, and a createdAt field. The id field uses a UUID, a unique string that can be generated by Prisma when a user is created.
Product ModelScott creates the schema for the Product model. A relationship between the Product and User models is established using the Prisma @relation attribute.
Update & UpdatePoint ModelsScott finishes the last two models, Update and UpdatePoint. Relationships are established between the two models. A question about how Prisma supports multiple database platforms is also answered in this lesson.
MigrationsScott runs a migration that syncs the database structure with the Prisma schema. A migrations directory is also created which stores every executed migration. The benefit of having migration files is the ability to track them with source control and ensure every developer is aware of changes made to the database.
Routes & Middleware
Defining RoutesScott explains the CRUD operations Create, Read, Update, and Delete. The routes required to execute these operations are created for the Product, Update, and UpdatePoint models. A question about GRPC vs REST is also answered in this lesson.
Importing the Application RouterScott mounts the router in the main application and configures it to be used under the "/api" parent route. This architecture makes the application more modular because the business logic for different areas of the application can be defined in separate routers.
Testing the API with Thunder ClientScott demonstrates using the VSCode extension Thunder Client to test API endpoints. An endpoint needs to return something. Otherwise, the request will hang. With HTTP, the connection with the server is closed once something is returned.
MiddlewareScott introduces the concept of middleware and installs Morgan, a logging middleware for Express. Middleware is a function that can be called for any request or only specific routes. The middleware function calls a next() method when it's complete so the request can finish being processed by the server.
Creating a Custom MiddlewareScott creates a custom middleware that adds a property to the request object. This property is now available to any subsequent middleware and the route handler.
Creating a JWTScott introduces JSON web tokens or JWTs. The purpose of a JWT is to help identify an authenticated user on the server. The JWT is created by combining the user's ID and username with a stored secret. This creates a deterministic string to identify the user.
Protecting RoutesScott begins implementing a middleware to protect the API routes. The middleware first checks to see if a bearer token is present. If not, it returns and 401 not authorized error.
Validating a Bearer TokenScott adds code to validate the JWT token. The validation logic ensures the token is well-formed. If not, an error is thrown, and the server again issues a 401 unauthorized request. A question about API rate limiting is also addressed in this lesson.
Authorization HeadersScott reviews how authorization headers enforce authentication rules on a server.
Comparing & Hashing PasswordsScott creates functions for comparing and hashing passwords. The hash function takes the user-submitted password, combines it with a salt, and returns a hashed password. The compare function will look at a plaintext password and determine if it matches a hashed version.
Creating UsersScott implements the createNewUser function, which hashes the password and creates and new user record in the database for that username and hash combination. The function also creates a JWT and returns the token.
Authenticating a UserScott implements the signin function which will search the database for a user with the submitted username and compare the submitted password with the hashed password for that user.
Adding User RoutesScott adds the routes for creating a user and signing in a user. Both routes result in a JWT being returned if the request is completed successfully. The JWT can then be passed as a bearer token to the protected API routes.
Route & Error Handlers
Validation OverviewScott explains why validating user input is important. There are many third-party libraries specializing in input validation. The express-validator module will be used in this application.
Adding Validation to RoutesScott adds the express-validator module to the API and uses it as middleware to check the existence of the name property on the body during a product update.
Route Validation Exercise & SolutionStudents are instructed to add validation to the remaining product, update, and updatepoint PUT/POST routes.
Get Product HandlersScott begins creating route handlers for the products table. One route handler returns all the user's products. The other returns a single product based on the ID passed to the route.
Create & Update Product HandlersScott codes the create and update product handlers. Both handlers will not only send a product name to Prisma but also the current user's ID to ensure the product that's created/updated belongs to the user.
Applying Product Route HandlersScott adds the product route handlers to the router and tests the application with Thunder Client.
Update Handlers Exercise & SolutionStudents are instructed to create route handlers for the Update and UpdatePoint routes. This lesson includes the solution to the getUpdate and getOneUpdate handlers.
Create & Delete Handlers SolutionScott continues the solution for the Update Handlers exercise. This lesson includes the solution to the createUpdate, updateUpdate, and DeleteUpdate route handlers.
Debugging RoutesScott uses Thunder Client to test and debug all the route handlers in the application. Once a user is authenticated, a product is created along with updates for the product.
Creating Error HandlersScott demonstrates how errors cause a Node server to shut down. Express has built-in error handling, however, the default behavior is to output the error message and stack trace. Creating error-handling middleware allows the application to respond differently depending on the error thrown.
Async Error HandlersScott uses the next function to handle an async error. Calling the next function with the error as a parameter sends the error to the next middleware and allows it to be handled like a synchronous error.
Error Handlers Exercise & SolutionStudents are instructed to add error handling to all the API route handlers in the application.
Handling Errors with process.on()Scott explains how errors can be handled outside of Express using the process.on API. Similar to using addEventListener, the "on" method has named events like uncaughtException and unhandledRejection which will trigger when certain errors are thrown.
Config, Performance, & Testing
Environment VariablesScott introduces environment variables. The NODE_ENV variable lets the application and any other package know the environment. This could be development, testing, production, or any other string. Using custom environment variables for database URIs or secrets allows confidential information to be injected into the application based on the environment.
Creating Environment ConfigurationsScott creates configuration files for each environment and uses the lodash.merge module to merge custom environment variables into a single configuration object. This architecture allows a default configuration to be created, and for each environment, a customization for a specific environment can override the default.
Performance Management with AsyncScott explains the difference between blocking and nonblocking code. Whenever possible, developers should use asynchronous APIs because they are nonblocking and more performant for the server when executing multiple requests.
Unit Testing OverviewScott describes unit tests as testing individual pieces of logic independently. In order for unit tests to be effective, the code should be written in a testable way. For example, function arguments are easier to test than closure variables.
Unit Testing with JestScott creates a __tests__ directory and writes a unit test. The describe block is a wrapper around one or more tests. The "it" method contains a statement about what outcome is expected for the individual test.
Integration Testing with SuperTestScott demonstrates how integration tests can be used to test an entire route's functionality. The supertest module provides methods for building requests and validating the result of those requests.
Testing the Create User RouteScott uses the supertest module to write an integration test for the createNewUser route handler. A mock object is sent along with the request with the username and password. The importance of using a testing database is also discussed in this lesson.
Deploying to RenderScott deploys the application to Render.com. When the code is committed to GitHub, Render.com will pull in the latest version of the code and redeploy it. Environment variables are also configured in the Render.com web service wizard.