Web‐Infra‐Week‐8 - TheEvergreenStateCollege/upper-division-cs-23-24 GitHub Wiki

Web Infra Setup - Week 8

(This is based on Scott Moss and his Frontend Masters Course v4, NodeJS API Design).

Back to Web Engineering '24 Winter

In the setup this week, we are going to create our Express server from scratch with Typescript. It is very similar to the Javascript Express server we've been working on since Week 3, so if you've already mastered that server, you'll be able to complete today's setup even faster.

And if you haven't completed or feel comfortable with the Javascript server, here's your chance to start from scratch with a more maintainable, strongly-typed version.

Intro

To fully-understand and pass a code interview based on this material, it is not enough to type in the code below. You will need to watch the videos linked to in the homeworks above, in addition to typing it out.

This material was first mentioned in

We began this setup for an Express API server in Week 3 in plain javascript.

This time, we are going through it again from scratch in Typescript. There are only a few minor changes, and the end result is nearly identical. If you already have a final project directory, you should add/commit any work in it first.

You can create a new week8 directory in your assignments and create this new Typescript Express server with authentication functionality, then copy it over to your final project directory when you are ready, and knowing that you can always git restore any files if you need to go back to an earlier version.

The difference is that Typescript provides optional typechecking and better linting in VSCode, features like auto-complete, auto-suggest, syntax checking, and more, to catch errors before you even run the code.

Typescript compiles down to Javascript with the tsc global package (Typescript Compiler) and also automatically compiles and runs it with the ts-node global package (Typescript version of NodeJS).

Setup

Pair Programming

This, and all tutorials, was designed to be done in pairs.

Who is a pair programming partner?

If you are in a final project team, one of your teammates will be your partner.

If your team is not present or you are working solo, turn your head to the left, then the right. If you see someone sitting at a computer nearest yours, and they don't have a partner yet, that is your pair programming partner. Introduce yourselves.

Pick one of you to be a driver, who is the person who will type first. The other person will be a navigator, who reads this tutorial and directs the driver what to type.

Directories

To work on this Node server in isolation from your final project, to copy over later:

cd <REPO_DIR>/assignments/<YOUR_GITHUB_USERNAME>
mkdir week8

However, you can also work directly in your final project directory, if you are comfortable "on-the-fly" deciding what to include, not include, or adapt from this tutorial to your final project.

cd <REPO_DIR>/projects/<YOUR_FINAL_PROJECT_NAME>

Over the next few sections, you will create files to match this initial directory tree

image

pnpm

We use the Parallel version of the Node Package Manager (pnpm) because it saves space across multiple NodeJS projects, such as happens in our class monorepo which contains projects from all our classmates.

To use it, we first install it globally, so that all projects can use it on a system, with the normal npm.

npm i -g pnpm

After this, you'll need to run a setup script to update your shell initialization file, usually ~/.bashrc. We'll call it that below, but substitute it in your mind if you use zsh, fish, or any other shell with a differently named startup file.

pnpm setup

Now when you install global binaries with pnpm, it will put them in the following path, which you can check by typing

pnpm bin

You can add this to your command-line PATH environment variable by sourcing your shell startup file

source ~/.bashrc

Or closing your shell and re-opening it again.

You can also manually or temporarily add the pnpm binaries to your PATH with the following command

export PATH=${PATH}:$(pnpm bin)

package.json

All NodeJS packages, even Typescript ones, are described in a package.json file in the root directory of the project. You can tell it's a Typescript project because of the presence of tsc, ts-node, and other Typescript related devDependencies.

We know they are development dependencies because all typechecking from Typescript happens at compile-time (development time, on your laptop). It then produces just plain Vanilla Javascript (optimized and minified), which then get run and served to your users (production or deploy time, on your AWS server).

As a general goal, we are a little more tolerant of devDependencies that help us do our job as engineers faster, but we want as few deploy-time dependencies because they slow down our app, take up space in our node_modules directory, and introduce complexity and possible security vulnerabilities.

To create an initial package file, make a directory and run

pnpm init

When you open package.json you will see some version of the screenshot below.

image

Step-by-step, we will work on duplicating all the lines you see, and many of them we won't need to type by hand.

You will need to add all the dependencies as follows.

Exercise 1: Run-time vs. Compile-time

A useful distinction in programming is between the time you are developing the code (compile-time) versus the time you are deploying or running the code (run-time). This is sometimes called development versus production environments. More about that later.

Time aka Node Environment English description Where does it happen
compile-time develop-time development "writing the code" "on your laptop"
run-time deploy-time production "running the code" "on your server"

We'll explain what each of the dependencies we use does, but we've mixed up the compile-time versus run-time dependencies in this list below. As you read the description, try and guess whether it's needed at compile-time (a devDependency) or at run-time (just a dependency).

  • @types/express - provides optional type annotations for developing with the Express middleware and server framework
  • express-validate - a wrapper library for validating strings that HTTP clients send us
  • @types/node - provides optional type annotations for developing with the NodeJS core libraries
  • bcrypt - a library for cryptographic hashes which we use as a one-way function to protect and validate passwords and authenticate HTTP clients when they try to log in
  • prisma - helps generate Prisma schema files, format them including fixing up relations between models, validate them to make sure there are no errors, generating a client for your language of choice (in this case JS/TS), and migrating the schema to your database manager (in this case Postgres).
  • @prisma/client - the generated Prisma client that runs when you handling HTTP requests and wanting to access your database
  • express - an HTTP middleware and server framework for handling client requests and sending back responses
  • jsonwebtoken - a library for creating ephemeral bearer tokens to send to authenticated HTTP clients based on our secret seed
  • morgan - a library for logging HTTP requests as they arrive and HTTP responses as they are sent out.

Dependencies

To add a run-time / deploy-time dependency called some-package1 you would run

pnpm i some-package1

and you can install all of them at once with a space-separated list

pnpm i some-package1 some-package-2 ...

You would then see it appear in the dependencies key in package.json. You'll also see it, and all its sub-dependencies, with specific version numbers, appear in pnpm-lock.yaml package-lock.json serves the same function for the original npm, and you don't need both of the lock files. You can delete whichever one belongs to the package manager that you decide not to use, and there is no harm in keeping both files around for now.

Dev Dependencies

To add a compile-time / development-time dependencies called another-package1, another-package2 and so forth, you would run

pnpm i -D another-package1 another-package2 ...

You would then see it appear in the devDependencies key in package.json. See the note above about lock files.

Scripts

Every package.json defines custom script commands that can be different for every project. Type these scripts for our project into package.json, replacing any scripts that have been auto-generated there, or adding them to scripts you have already defined.

Be sure to end each of these lines with a comma except the last one.

  • "build": "tsc",
  • "test": "echo \"Error: no test specified\" && exit 1",
  • "dev": "npm run build && node ./dist/index.js"

tsconfig.json

There are a few Typescript options we'll be using. You can manually type these into a tsconfig.json file, also in the root of your project and a sibling to package.json.

ESNext refers to the latest version of ECMAScript, the technical name for Javascript, changes that are on the track to become a standard but are not released yet. More details here

The ./dist directory is where the Javascript / ES5 / CommonJS version of all our Typescript code gets saved. This is where we run code with most NodeJS tools, including the main node runtime, since they were created before the most modern versions of ES6 and Typescript became widespread.

image

Source Files

We are going to create the following source files, starting with creating a src directory to keep our sources separate from project files, Prisma schemas, etc.

mkdir src
touch src/index.ts
touch src/server.ts

Our server will be short and simple at first, so we'll only need two files. It will do some basic error-checking and set up some generally-useful middleware for later.

index.ts

image

This file is the entry point of our server. That is, we enter and run this module first with the node command. You can verify that in package.json, our dev script is indeed:

node ./dist/index.js

server.ts

image

Compile - Debug - Repeat

Now we're ready to build our server and check for errors.

pnpm run build

This will compile (some may say transpile) our Typescript code in src into Javascript code in dist.

Don't worry if you get error messages in this stage. Here's our recommended debugging method in order:

  1. Try to understand what the errors mean and what line numbers they occur on. Fix them in VSCode or your favorite text editor, and try again. Check the screenshots above and double-check your work.
  2. Ask your pair programming partner for help first. Who is a pair programming partner? Turn your head to the left, then the right. If you see someone sitting at a computer next to yours, that is your pair programming partner. When the moment is right, and they are similarly stuck, say hi.
  3. If that doesn't work, formulate a natural language English question and try an internet search engine.
  4. Raise your hand for teaching staff.

Yes, I know some of you are going to reach first and often for AI chat. Instead, I recommend learning to meditate and exercise your own problem-solving muscles enough until you have some ability to sit with discomfort and not-yet-understanding. Relying on AI too early to solve computational problems will be like driving a forklift when helping a friend move out of their apartment. It may get some heavy jobs done quickly, but it won't relieve you of human judgment. And if you rely on it as your first instinct, you're going to get some strange results and maybe more extra holes punched than you were expecting.

It doesn't have any routes beyond the root route yet, but let's hit it to see the phrase that warms all cold robotic hearts everywhere.

image

Router and User Signin

Now we'll add the ability for users to sign-in with a password.

We'll create four files

  • a top-level router for handler methods
  • a module for handling authentication middleware
  • a module for general input validation (making sure we have a valid JSON request body, for example)
  • a handler for creating and reading users
mkdir src/handlers
mkdir src/modules
touch src/router.ts
touch src/modules/auth.ts
touch src/modules/middleware.ts
touch src/handlers/users.ts

After these commands, when you run your tree command you should see this

$ tree -I node_modules -I dist── package.json
├── pnpm-lock.yaml
├── prisma
│   ├── migrations
│   │   ├── 20240229102550_init
│   │   │   └── migration.sql
│   │   └── migration_lock.toml
│   └── schema.prisma
├── public
│   ├── index.html
│   └── map.html
├── README.md
├── src
│   ├── db.ts
│   ├── handlers
│   │   └── users.ts
│   ├── index.ts
│   ├── modules
│   │   └── auth.ts
│   ├── server.ts
│   └── types.ts
└── tsconfig.json

src/db.ts

The file db.ts encapsulates some imports from Prisma and creates the client once. (NodeJS modules are singletons, and run their initialization once when they are first loaded in a NodeJS execution).

image

src/types.ts

We are breaking out typescript types here, especially since we are trying to augment the normal Express Request type with an optional extra field, storing the logged in user.

This part doesn't quite work yet. I am working through this LogRocket tutorial for it.

image

modules/auth.ts

This module contains utility functions such as hashing a password, compare a password with its hash, and a middleware function to insert into a sub-router later to protect a group of routes.

image image

modules/users.ts

image image

JWT_SECRET

You'll need to create a .env file to store your secrets and other environment variables for each deploy, which should not be committed into GitHub.

You can generate a new secret with

openssl rand -base64 32

And you can copy the output and save it into .env so it looks like this

image
⚠️ **GitHub.com Fallback** ⚠️