Project 1 — URL shortener: what we are building
The plan, the endpoints, and the data before a single line of Go.
- Explain the URL shortener's endpoints and data before writing code.
- Explain the package-by-layer architecture and its one dependency rule.
- Explain what the project deliberately leaves out, and why.
What you will build, and what it looks like when done
A small web service. You send it a long URL, it gives you a short code. Open the short code and it redirects you to the long URL. Here is the finished thing in a terminal. The rest of this page is the plan for getting there.
$ curl -s -X POST localhost:8080/links -d '{"url":"https://go.dev/doc/effective_go"}'{"code":"aB3xK","long_url":"https://go.dev/doc/effective_go","clicks":0}$ curl -si localhost:8080/aB3xK | head -2HTTP/1.1 301 Moved PermanentlyLocation: https://go.dev/doc/effective_goWhat a URL shortener does
A URL shortener takes a long web address and gives back a short one. When somebody opens the short one, your server looks up the original and sends the browser there. That is the whole product.
It is the right first project because it is small enough to finish, and it still contains almost everything a real service has: input you cannot trust, an id you have to generate, storage, a lookup on the hot path, and a redirect.
The architecture: layered (package by layer)
Each of the three projects in this track uses a different architecture on purpose, so you end up having built all three rather than read about them. This one is layered, also called package-by-layer or the n-tier layout. You will meet it often in tutorials and in older codebases.
The idea is simple: a folder for each kind of code. Types in one place, storage in another, rules in another, HTTP in another. The folder answers the question *what kind of code is this?*
- Model (
internal/model) — the types and their errors. - Store (
internal/store) — putting data somewhere and getting it back. - Service (
internal/service) — the rules: validation, code generation. - Transport (
internal/transport/web) — reading requests, writing responses. - Entry point (
cmd/api) — build everything and start it.
The one rule
Dependencies point downwards only: transport → service → store → model. Nothing lower ever imports something higher. store must never import transport, and model must import none of them.
Most of the layout’s value comes from that rule. It lets you test the service without an HTTP server, and swap the store from a map to Postgres in step 5 without touching the service or the handlers.
cmd/api -> transport, service, store, modeltransport/web -> service, modelservice -> model (+ a store interface it declares itself)store -> modelmodel -> nothing// Every arrow points one way. If you ever need one to point back, the code// on the far end belongs somewhere else.What it is good at, and what it is not
- Good: it is obvious where a new file goes, easy to explain, and the layer boundaries are easy to police. Perfect for one or two features.
- Not so good: one feature is spread across four folders, every folder grows with each feature, and it strains at around ten features.
Decide the endpoints first
Before writing code, write down what the API accepts and what it returns. Doing this first saves you from rewriting handlers when you discover the shape was wrong.
| Method and path | What it does | Returns |
|---|---|---|
POST /links | shorten a long URL | 201 with the short code |
GET /links | list the links you have made | 200 with a list |
GET /links/{code} | details and click count for one link | 200, or 404 |
DELETE /links/{code} | remove a link | 204, or 404 |
GET /{code} | the redirect — the actual product | 301 to the long URL, or 404 |
GET /healthz | is the service alive | 200 |
GET /{code} sits at the root, not under /links, because short URLs should be short: sho.rt/aB3xK beats sho.rt/links/aB3xK. Make that decision now, because it changes how you register routes.Decide the data
One thing is being stored, so there is one table. Write it down before you write Go, because the struct will follow the table rather than the other way round.
id bigserial the database's own keycode text, unique the short part of the URL — what people typelong_url text where to send themclicks bigint how many times it has been openedcreated_at timestamptz so you can sort and expire old linksThe plan, step by step
You will build this in six steps. Each step adds a few files, and after every step the program still runs. That matters: if you write every file and only then run it for the first time, you will be debugging everything at once.
- Step 1 — set up the module and the layered folders, with a health check that works.
- Step 2 — the store, in memory, so you can build the API before touching a database.
- Step 3 — creating a link: validating the URL and generating a code.
- Step 4 — the redirect, which is the actual product, plus click counting.
- Step 5 — move the store to Postgres. Nothing else changes.
- Step 6 — list and delete links, then graceful shutdown, a test and a Dockerfile.
What this project does not have
No user accounts, no login, no permissions. Anyone can create a link and anyone can read the list. That is on purpose: this project is about getting one service right end to end. Accounts arrive in project two, and they are easier to understand once you have built a working service without them.
- Write the endpoint table and the data table before any Go. It takes minutes and saves rewrites.
- The redirect lives at the root,
GET /{code}, so the short URLs stay short. - Build it in steps where the program runs after each one, not in one go.
- No accounts in this project. One thing at a time.
- 1Look at a real shortener and see which of these endpoints it has.
- 2Work out how many links a 5-character code can hold if you use letters and digits.
- 3Decide what should happen if somebody shortens a URL that is already shortened.