Building a Go REST API From an Empty Folder — and Actually Understanding It

Building a full-stack task board in Go and Gin, one understood line at a time. This study log covers standing up the server, diagnosing a stubborn port conflict with ss and docker ps, and returning tasks as JSON — with the security defaults the tutorial glosses over.

A study log: Go + Gin, Chapters 0–3

Tonight I started building a full-stack task board: a Go and Gin REST API on the backend, with MongoDB and a React kanban board still to come. But the real story here isn’t the code. It’s that I went slow enough to understand every line instead of copy-pasting a tutorial I couldn’t explain afterward.

Every command got broken down. Every error got diagnosed instead of skipped. When the server refused to start, I didn’t just try another port at random — I found out what was holding the port and why. This post is my checkpoint before Chapter 4, written while it’s fresh so I can review it before the next session.

The stack and the plan

Backend: Go (the language) with Gin, a web framework that makes defining routes and returning JSON quick. Storage: starts in memory, then moves to MongoDB running in Docker for real persistence. Frontend (later): React with Vite, built up from a plain list into a kanban board.

Tonight covered the first slice of that: confirming the toolchain, standing up a Gin server, and giving it a read-only tasks endpoint. In tutorial terms, Chapters 0 through 3 — a working API that can read tasks, with creating, updating, deleting, and the database still ahead.

Getting the server alive

Three steps took me from an empty folder to a running web server:

  • go mod init taskboardapi creates go.mod, the project manifest that names the module and tracks its dependencies — Go’s rough equivalent of a requirements.txt or package.json. I used a one-word module name on purpose: it becomes the import prefix for every package later, and Go module names can’t contain spaces.
  • go get github.com/gin-gonic/gin downloads Gin and its dependencies into Go’s shared module cache. The packages aren’t copied into the project — go.mod just references them, so one cache serves every project.
  • A minimal main.go: build a router (the switchboard that reads each request’s address and decides which function answers it), register a GET / route that replies with JSON, and call router.Run(":8090") to start listening.

One thing the tutorial glides past, worth noticing early: router.Run() returns an error, and the code ignores it. In Go, error handling is deliberately explicit, and quietly dropping a returned error is exactly the habit that bites you later. I left it as-is to match the lesson — but I clocked it. (That ignored error is also what made the next problem show up as a clean message instead of a silent failure.)

The best lesson of the night: a port conflict

The server wouldn’t start. The last line of the log said:

[ERROR] listen tcp :8080: bind: address already in use

The tempting move is to shrug and try a different port. The better move — and the one that turned this into the most useful part of the night — is to find out what already owns the port. That’s pure reconnaissance, and it’s the same instinct that matters far beyond this project.

The tool is ss (“socket statistics”), the modern successor for netstat. A socket is one endpoint of a network connection: an IP address plus a port. The command:

ss -ltnp 'sport = :8080'

reads, piece by piece: -l listening sockets only (servers waiting, not active conversations), -t TCP only, -n numeric (show raw port numbers, don’t translate them to friendly names — in security work you want ground truth), -p show the owning process. The quoted 'sport = :8080' is a filter: source port equals 8080. The single quotes keep the shell from chopping the expression apart at the spaces.

The first run showed the port was taken but left the process column blank — because seeing which process owns a socket needs privilege. Re-running with sudo filled it in: the owner was another container already published on 8080. docker ps (container “process status”) confirmed which one.

The fix wasn’t to fight for the port — it was to move my app to a free one. And I confirmed the new port was actually free with the same ss command first (header row only, no LISTEN lines = nothing there), rather than guessing and hitting the wall again.

The takeaway that sticks: address already in use is not a bug in your code. It’s a fact about the state of the machine. ss and docker ps tell you the truth about what is listening and who owns it — and knowing what’s listening on a box is step one of understanding it.

Giving the server real data

With the server alive, the next step was to hand it some tasks. Two additions to main.go.

First, a struct — the blueprint for what a single task looks like:

type Task struct {
	ID     int    `json:"id"`
	Title  string `json:"title"`
	Desc   string `json:"desc"`
	Due    string `json:"due"`
	Status string `json:"status"`
}

A struct groups related fields under one name, like a form template every task must fill in. The interesting part is the backtick tags — `json:"title"`. Go’s convention is that exported field names are capitalized (Title), but JSON on the web is conventionally lowercase (title). The tag is a translation label: “when you turn this into JSON, call this field title.” That naming has to match the frontend later, so picking names and staying consistent matters.

Second, an in-memory slice (Go’s word for a list) of tasks, seeded with two entries, and a GET /tasks route that returns the slice. Gin turns it into a JSON array automatically, using the tags for field names. Browsing to /tasks returned:

[{"id":1,"title":"Learn Gin","desc":"","due":"2026-11-12","status":"planned"}, ...]

Two honest caveats the tutorial doesn’t dwell on. The slice lives in the program’s memory, so the data resets every time the server restarts — real persistence arrives with MongoDB in a later chapter. And the shared slice has no concurrency lock, so simultaneous writes from multiple users could corrupt it. Neither matters while learning solo, but both are real, and worth knowing they exist rather than discovering them the hard way.

What I actually learned (concepts, not keystrokes)

Stripping away the specific commands, here’s what I can now explain in my own words:

  • The request/response loop from the server’s side. I’ve written clients that hammer endpoints before. This is the first time I’m the server answering the request — the other half of the exchange.
  • Routing and HTTP GET — how a router maps an incoming address to the function that handles it, and that GET is the “read” verb browsers send by default.
  • Structs and struct tags — a data blueprint, plus a translator between Go’s field names and the JSON names on the wire.
  • Slices as Go’s lists, and why in-memory storage is temporary.
  • Diagnosing a port conflict with ss and docker ps instead of guessing.

And two security notes that came straight out of Gin’s own startup log, the kind of thing a copy-paste run would scroll right past:

  • Gin warned it was running in debug mode (“switch to release mode in production”) — a real environment-specific config concern.
  • Gin warned “you trusted all proxies, this is NOT safe.” By default it trusts forwarded-IP headers (X-Forwarded-For) from any client, which means a caller could spoof their apparent address. Not exploitable in a local build, but exactly the kind of insecure default worth clocking early.

Next: Chapter 4

Right now everything lives in one main.go. That’s fine for a read-only demo, but it doesn’t scale. Chapter 4 is the biggest structural jump in the whole build: splitting the code into three packages — models (what a task is), store (where tasks live and the logic to change them), and handlers (the functions that answer web requests) — and then adding the ability to create a task with a POST request.

That’s how real Go projects are organized, and it’s a meatier session than the quick edits so far, which is exactly why I’m reviewing this checkpoint before diving in.