Web‐Infra‐Week‐8 - TheEvergreenStateCollege/upper-division-cs-23-24 GitHub Wiki
(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.
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).
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.
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

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)
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.

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.
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.
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.
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.
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"
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.

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.

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

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:
- 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.
- 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.
- If that doesn't work, formulate a natural language English question and try an internet search engine.
- 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.

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
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).

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.

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.




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
