Build one Go server and one TypeScript server that each claim a port and wait forever, then prove they are listening and learn to read the two errors beginners confuse most.
Learning objectives
Explain what a server and a port are in plain language.
Create the project folder both languages will share for the whole course.
Write and run a minimal HTTP server in Go and in TypeScript.
Tell "connection refused" apart from "404 Not Found" and say which layer each one blames.
Where this course is going
Two programs need to call each other. One is written in Go, one in TypeScript. That is the entire setup, and almost every security question in backend engineering shows up inside it.
By the end of the course, every call between them will carry two independent proofs:
the destination
which service is calling → proven by a certificate that expires in 60 seconds
which user it is for → proven by a token that expires in 60 seconds
Right now we have neither. We do not even have two programs. So we start there.
What you need
Go (any recent version), Node 22.18 or newer, and a terminal. Nothing is installed with a package manager anywhere in this course.
Three things arrive later that are worth knowing about now, so they do not stop you halfway:
From
You also need
Why
Lesson 4
curl
Testing every endpoint from the command line
Lesson 5
openssl
Creating your own certificate authority
Lesson 11
Linux or WSL, and nc
The workload API uses SO_PEERCRED and reads /proc, which are Linux features
Lessons 1 to 10 run on macOS, Linux, and Windows alike. Chapter 5 does not: if you are on macOS or Windows, plan to use WSL, a Linux virtual machine, or a container for the last four lessons.
You do not need any prior knowledge of servers, networks, cryptography, or security. Every one of those ideas is introduced here from zero.
You do need to be able to read basic Go and basic TypeScript: a function, a loop, an if, a struct or object. If you have written a small program in either language, you have enough. Where the two languages do something genuinely unusual — Go's go keyword, Node's promises, TypeScript's type assertions — the lesson stops and explains it.
Two words before any code
A server is a program that does not finish
Forget the picture of a humming machine in a data center. That is a computer. A server is a program.
Normal programs run from top to bottom and exit. A server runs from top to bottom and then enters a loop that asks "is anyone there? is anyone there?" forever.
That is genuinely the whole idea. Everything else in this course is about deciding who gets an answer.
A port is a room number
Your computer has one network connection but runs many programs at once. When a message arrives, how does the operating system know which program it belongs to?
The port number: a number from 1 to 65535 attached to every message.
In simple words
Think of your computer's address as a building and the port as a room number inside it. The mail reaches the building; the room number decides whose desk it lands on.
We will use 8091 for the Go service and 8092 for the TypeScript service. Nothing is special about those numbers. We only need two that nothing else on your machine is using.
Create the project
Everything in this course lives in one folder that grows lesson by lesson. Create it now:
zsh — 80×24
$mkdir secure-services
$cd secure-services
$git init -b main
$go mod init secure-services
$go mod edit -go=1.21
$
go mod init creates a file called go.mod. Go needs it to know where your project starts, and you only ever run it once.
Two of those lines deserve a closer look:
The module path.secure-services is this project's permanent import identity. Keep the exact name while following the course, because Lesson 12 creates a package that other files import as secure-services/spiffe. If you rename the module later, rename every import in the same commit.
The minimum Go version.go mod edit -go=1.21 declares what the project needs, not what is installed on your machine. Your toolchain is probably newer, and that is fine. Lesson 12 uses the slices package, which arrived in Go 1.21, so 1.21 is the honest floor.
Your folder now contains exactly one file:
repository
secure-services/
└── go.mod
Write the Go server
Create a new file called main.go. We will build it in three pieces and then look at the whole thing.
Piece 1 — the header. Every Go file starts with a package name, then lists the libraries it uses:
main.goGo
package mainimport ( "fmt" "log" "net/http")
package main means "this program can be run"
fmt prints messages
log prints messages with a timestamp
net/http is everything to do with web servers
Piece 2 — the port. Add this below the imports:
main.goGo
const port = "8091"
A const is a value that never changes. Putting it at the top means you change the port in one place instead of hunting through the file.
Piece 3 — the program itself. Add this at the bottom:
main.goGo
func main() { // A "mux" decides which code runs for which URL path. // Ours is empty, so every request gets a 404. That is fine for now. mux := http.NewServeMux() fmt.Printf("The Go Server is running on port %s\n", port) // This line never finishes. It waits for connections forever. log.Fatal(http.ListenAndServe("127.0.0.1:"+port, mux))}
The complete Go file
Your main.go should now look exactly like this:
main.goGo
package mainimport ( "fmt" "log" "net/http")const port = "8091"func main() { // A "mux" decides which code runs for which URL path. // Ours is empty, so every request gets a 404. That is fine for now. mux := http.NewServeMux() fmt.Printf("The Go Server is running on port %s\n", port) // ListenAndServe blocks forever. It only returns if something goes wrong, // so reaching log.Fatal means the server died. log.Fatal(http.ListenAndServe("127.0.0.1:"+port, mux))}
Run the Go server
zsh — 80×24
$go run .
$
You should see:
zsh — 80×24
$The Go Server is running on port 8091
$
And then nothing happens. Your terminal appears frozen.
That is correct. Do not press Ctrl+C yet.
Memory shortcut
A server that hangs is a server that is working. A server that gives your prompt back has stopped.
Prove it is really listening
Leave the Go server running. Open a second terminal in the same folder and run:
zsh — 80×24
$curl -i http://127.0.0.1:8091/
$
curl makes a web request from the command line. The -i flag means "show me the response headers too".
You should see:
responseHTTP
HTTP/1.1 404 Not Found
404 is an error. Did it fail?
No. This is a success.
404 Not Found means something answered you, but it has no page at that address. Our mux is empty, so there is no page anywhere. The important part is that something answered.
Now go back to the first terminal, press Ctrl+C to stop the server, and run the same curl command again:
zsh — 80×24
$curl: (7) Failed to connect to 127.0.0.1 port 8091: Connection refused
$
Connection refused means nothing is listening on that port at all.
Learn these two apart right now
You see
It means
Where to look
Connection refused
Nothing is listening on that port
Is the server running? Right port?
404 Not Found
Something is listening, but has no such page
Your routes, your URL spelling
Confusing these two sends people down the wrong path for hours. The first is a network problem: your request never reached a program. The second is an application problem: a program received your request and decided it had nothing for you.
You will meet both again in every remaining lesson, so it is worth spending a minute making the difference solid now.
Write the TypeScript server
Now the same thing in TypeScript. Start the Go server again first (go run . in the first terminal).
In the same secure-services folder, create server.ts.
Piece 1 — the import and the port:
server.tsTypeScript
import { createServer } from "node:http";const PORT = 8092;
node:http is Node's built-in web library. The node: prefix means "this is built in, not something I installed".
Piece 2 — the server:
server.tsTypeScript
// This function runs for EVERY incoming request.// Ours answers 404 to everything, because we have no routes yet.const server = createServer((req, res) => { res.writeHead(404); res.end();});
res.writeHead(404) sets the status code. res.end() says "I am finished replying".
Piece 3 — start listening:
server.tsTypeScript
server.listen(PORT, "127.0.0.1", () => { console.log(`The TypeScript Server is running on port ${PORT}`);});
The function passed to listen runs once, when the server is ready. Everything after that is the waiting loop.
The complete TypeScript file
server.tsTypeScript
import { createServer } from "node:http";const PORT = 8092;// createServer takes a function that runs for EVERY incoming request.// Ours answers 404 to everything, because we have not built any routes yet.const server = createServer((req, res) => { res.writeHead(404); res.end();});// listen() starts accepting connections. The callback runs once, when the// server is ready. After that the process just sits here forever.server.listen(PORT, "127.0.0.1", () => { console.log(`The TypeScript Server is running on port ${PORT}`);});
Run the TypeScript server
In a third terminal:
zsh — 80×24
$node server.ts
$
zsh — 80×24
$The TypeScript Server is running on port 8092
$
No compiler, no npm install
Node 22.18 and later run .ts files directly by removing the type annotations before executing. There is nothing to install in this entire course. On Node 22.6 through 22.17 the same thing works with node --experimental-strip-types server.ts.
Test it
zsh — 80×24
$curl -i http://127.0.0.1:8092/
$
responseHTTP
HTTP/1.1 404 Not Found
Same answer as the Go server, for the same reason: our handler replies 404 to everything, because we have not built any routes. Something answered you, which is all we are checking today.
Why 127.0.0.1 and not just the port?
Look at both programs. Neither says only "listen on port 8091". Both say 127.0.0.1:8091.
127.0.0.1 is the loopback address. It means this machine only. Nobody else on your network can reach it.
The alternative is 0.0.0.0, which means "any network this machine is connected to". If you used that, the deliberately unprotected practice server you just wrote would be reachable by anyone sharing your wifi.
Production warning
Every step in this course binds to 127.0.0.1 on purpose, because for the next three lessons these servers have no security at all. When you build something real, changing this line is a decision, not a detail.
Another program holds that port, often an old copy of your own server
Stop it, or change the port number in both files
go: cannot find main module
You skipped go mod init
Run go mod init secure-services in the folder
ERR_UNKNOWN_FILE_EXTENSION .ts
Your Node is older than 22.18
Run node --version, then upgrade or add --experimental-strip-types
command not found: go
Go is not installed or not on your PATH
Install Go, then open a new terminal
Lesson 1 checkpoint
You are ready for Lesson 2 when you can explain all of these without rereading:
why a server is a program that does not finish;
why only one program at a time can listen on port 8091;
why a frozen terminal after the startup message is correct;
what Connection refused proves that 404 Not Found does not;
what changes if you bind to 0.0.0.0 instead of 127.0.0.1.
Right now the two programs are strangers running side by side. Next you will give each one an endpoint the other can call, and watch the first real problem appear: neither server has any idea who is on the other end.