Project setup
We spent the last two lessons talking about problems. Now we fix them. Before we can build anything interesting, though, we need an actual server to put code into. This lesson is the setup step: we will install Node.js, create a fresh project, add a small HTTP framework called Hectoday HTTP, and run a tiny hello-world server to make sure everything is wired up. Nothing exciting happens in this lesson on its own, but every single lesson after this one will assume you have the project from here working. So let’s get it right.
What we are building (eventually)
Throughout this course, we are going to build an authentication system piece by piece. Each lesson adds something new. By the end, you will have a working API with signup, login, logout, protected routes, and role-based access control. That is the destination. Today, we are just buying the tickets and packing the bags.
Here is the toolbox we are going to use. Don’t worry about memorizing this. We will install each piece as we need it and explain what it does.
- Node.js, the JavaScript runtime that runs our server
- TypeScript, which is JavaScript with type annotations so our editor can catch mistakes early
- Hectoday HTTP, a small HTTP framework built on Web Standards
- srvx, a bridge that lets Hectoday HTTP run on Node.js
- Zod, a validation library for checking incoming request data
- bcrypt, for hashing passwords (we add this in Section 2)
- jose, for creating and verifying JWTs (we add this in Section 4)
We will install bcrypt and jose when we actually need them. For now let’s just get the basics running.
Prerequisites
You need Node.js 20 or later installed on your machine. You can check your version like this:
node --version
# v20.x.x or higher If you do not have Node.js installed, grab it from nodejs.org. The LTS (Long Term Support) version is what you want. It is the stable one.
You will also need npm, which comes bundled with Node.js. Check it with:
npm --version
# 10.x.x or higher If both of those show version numbers, you are good to go.
Create the project
Make a new directory and initialize it as a Node project:
mkdir auth-course
cd auth-course
npm init -y The -y flag just says “accept all the defaults, don’t ask me questions.” This creates a package.json file, which is basically the project’s manifest. It tracks which dependencies you have installed, any scripts you want to run, and so on. We will come back to it in a minute.
Install the dependencies
Run these three install commands:
npm install @hectoday/http zod
npm install srvx
npm install -D typescript @types/node tsx Let’s go through what each thing is doing here, because this is the first time you are seeing a lot of these names:
@hectoday/httpis the framework itself. It handles routing (mapping URLs to code), request parsing, and validation integration.zodis a validation library. We will use it to describe what we expect incoming request bodies to look like (is the email field actually an email? is the password at least 8 characters?).srvxis a little adapter. Hectoday HTTP speaks Web Standards, meaning it works withRequestandResponseobjects the same way modern browsers do. Node.js has its own older HTTP interface that predates that standard. srvx translates between the two so our modern-style code can run on Node.typescriptis the TypeScript compiler. We will not run it directly, but other tools need it installed to work.@types/nodegives TypeScript the type definitions for Node’s built-in APIs. Without this, TypeScript would complain that it has never heard ofprocessorBuffer.tsxis a little wrapper that runs TypeScript files directly. Without it, you would have to compile your.tsfiles to.jsand then run the compiled output. With tsx, you just saytsx server.tsand it does both steps in one go.
The -D flag in the third command means “install these as dev dependencies.” These are tools we only need while we are developing, not at runtime in production.
Configure TypeScript
Create a file called tsconfig.json in the project root with this content:
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"rootDir": "./src",
"outDir": "dist",
"types": ["node"]
},
"include": ["src"]
} TypeScript configs can be intimidating, so let me translate the important bits:
"strict": trueturns on all of TypeScript’s strictness checks. This catches a lot of bugs at compile time instead of at runtime. You want this on."module": "ES2022"tells TypeScript we are using modern ES module syntax (importandexport), not the older CommonJS (require)."moduleResolution": "bundler"matches how modern tools (including tsx) resolve imports. Older values here cause confusing errors in 2024-era Node projects."rootDir": "./src"and"include": ["src"]tell TypeScript that our source code lives in a folder calledsrc. We will create that in a second.
Set up the project structure
Make a src directory:
mkdir src Now we are going to create two files inside src. This two-file split matters, so let me explain why before we write the code.
We are going to separate defining the app from running the server. The app (app.ts) is the object that knows about routes and how to handle requests. The server (server.ts) is what actually listens on a network port. They are different concerns. Keeping them separate means that later, when you write tests, you can import the app directly and call its fetch method in memory without opening a real network socket. If the two were jammed into one file, importing the app would always start a real server, which is slow and gets in the way.
src/app.ts
// src/app.ts
import { setup, route } from "@hectoday/http";
export const app = setup({
routes: [
route.get("/health", {
resolve: () => Response.json({ status: "ok" }),
}),
],
}); Let’s walk through this slowly. setup() is the Hectoday HTTP function that creates an app. You pass it a configuration object. The only thing we are configuring right now is a routes array, which lists the endpoints the app knows about.
route.get("/health", { ... }) defines a single route. It says: when a GET request arrives at the path /health, run the resolve function. Our resolve function just returns Response.json({ status: "ok" }), which is a standard Web API. It creates an HTTP response with a JSON body ({"status": "ok"}) and automatically sets the content-type: application/json header. No framework helper is needed for that. It is just the browser’s built-in Response API, which Node also supports.
The setup() call returns an object that has a fetch method. That method takes a Request and returns a Response. This is the Web Standards server signature, the same shape that modern runtimes like Cloudflare Workers, Deno, and Bun all understand.
src/server.ts
// src/server.ts
import { serve } from "srvx";
import { app } from "./app.js";
serve({ fetch: app.fetch, port: 3000 }); Three lines. Import serve from srvx, import our app, then call serve() with the app’s fetch method and a port number.
Wait, why does the import path say ./app.js when the file is actually app.ts? This trips up almost everyone on their first try. TypeScript, when configured for ES modules, requires you to write the import path as if it were already compiled. You write the file as .ts, but you import it as .js because that is what the compiled output will be. It feels weird but it is how the module resolution works. Do not type ./app.ts or ./app. It must be ./app.js.
Add a start script
Open package.json and add a type field and a scripts entry:
{
"type": "module",
"scripts": {
"dev": "tsx watch src/server.ts"
}
} Two things are happening here:
"type": "module"tells Node to treat.jsfiles as ES modules. Without this, Node defaults to CommonJS, and yourimportandexportstatements will blow up with confusing syntax errors.- The
devscript usestsx watch, which runs the server and automatically restarts it whenever you save a file. No more manual stop-and-start during development.
Run it
From your project directory:
npm run dev You should see output indicating the server is running on port 3000. Open a second terminal window and hit the endpoint with curl:
curl http://localhost:3000/health You should see:
{ "status": "ok" } Now try a route that does not exist:
curl http://localhost:3000/nonexistent { "error": "Not Found" } Hectoday HTTP returns a 404 automatically when nothing matches. You can customize that later with a hook, but the default is fine for us.
What we have so far
Nothing about authentication yet, and that is the point. All we wanted in this lesson was a minimal project where the tools work and you understand the shape of the codebase. Here is your layout:
auth-course/
package.json
tsconfig.json
src/
app.ts # defines the app (routes and hooks)
server.ts # starts the server That is the skeleton. Every lesson after this one will either add a new file under src/ or extend one of these two. If you ever get lost, come back to this structure and remember: app.ts is where things happen, server.ts just turns the lights on.
In the next section, we finally get to the interesting part. We start building authentication for real, starting with something that seems obvious until you think about it carefully: how the heck do you store a password?
Why do we put the app in a separate file from the server?
What does srvx do?