Dev Skills

Step 1 — Set up the project

The module, the folders, and one route that proves it runs.

Topic 59 of 98 · 5 min to read · 45 min to build
After this topic you can
  • Create the module and folder layout for the shortener.
  • Write a JSON response helper and a main.go with one health route.
  • Run the server and check it with curl.

Every command for this step

terminal — run these in order
# 1. create the project
mkdir shortener && cd shortener
go mod init github.com/you/shortener
# 2. folders and empty files
mkdir -p cmd/api internal/transport/web
touch cmd/api/main.go internal/transport/web/respond.go
# 3. write the two files below, then
# (go mod tidy adds and removes dependencies; see "Modules and tooling")
go mod tidy
go run ./cmd/api
# 4. in a second terminal
curl -s localhost:8080/healthz
curl -si -X POST localhost:8080/healthz | head -1 # 405, for free

What this step gives you

By the end of this step you will have a running HTTP server that answers one route. No links and no database yet, only proof that the module, the folders and the server work.

Start every project this way. A server you can curl is a foundation; seven files written blind are a debugging session.

Create the module

The first two commands above create the folder and the module.

The module path is the import prefix for every package inside. Use your real repository path if you have one.

The folders you are starting with

shortener/
go.modjust created
cmd/runnable programs
api/
main.gofile 1 — wiring only
internal/private to this project
transport/everything that speaks HTTP
web/
respond.gofile 2 — JSON and error helpers
Folders are named after the JOB the code does, not the thing it is about. That is the layered style.

File 1 — internal/transport/web/respond.go

Start with the helpers, because every handler you write afterwards will use them. Two functions: one writes any value as JSON, one writes an error in a consistent shape.

internal/transport/web/respond.go — create this file
package web
import (
"encoding/json"
"net/http"
)
// JSON writes v as the response body with the given status code.
func JSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status) // headers first, then status, then body
if v != nil {
json.NewEncoder(w).Encode(v)
}
}
// Error writes one consistent error shape, so clients never have to guess.
func Error(w http.ResponseWriter, status int, msg string) {
JSON(w, status, map[string]string{"error": msg})
}
Every error your API returns will look like {"error": "..."}. Deciding that once, here, is why it stays consistent.

File 2 — cmd/api/main.go

main.go registers one route and starts a server. main does nothing but call run() and exit on error. os.Exit skips deferred calls, so keeping it out of run means every defer you add there later still runs.

cmd/api/main.go — create this file
package main
import (
"log/slog"
"net/http"
"os"
"time"
"github.com/you/shortener/internal/transport/web"
)
func main() {
if err := run(); err != nil {
slog.Error("fatal", "err", err)
os.Exit(1) // the only os.Exit in the program
}
}
func run() error {
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
web.JSON(w, 200, map[string]string{"status": "ok"})
})
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 15 * time.Second,
WriteTimeout: 30 * time.Second,
}
slog.Info("listening", "addr", srv.Addr)
return srv.ListenAndServe()
}

Run it

terminal
go run ./cmd/api
# in another terminal
curl -s localhost:8080/healthz
# {"status":"ok"}
If you get "no required module provides package", check that the import path matches the module name in go.mod exactly.
Note: The timeouts on http.Server are here from the first line rather than added later. The zero value means no timeout, so a client that sends its headers one byte at a time can hold a connection open for as long as it likes.
The short version
  • Get one route answering before you build anything else.
  • main calls run() error and exits. There is one os.Exit, outside run, so defers keep working.
  • Decide your JSON error shape once, in a helper, not in each handler.
  • Set server timeouts from the start, not as a later fix.
Remember: A server you can curl is a foundation you can build on. Files written before anything runs are debt.
Build this before moving on
Create both files, run the server, and get a response from /healthz. Then try POST /healthz and see that you get 405 without writing any code for it.
Then try these in your own editor
  1. 1Add a GET /version route that returns a hardcoded version string.
  2. 2Remove the Content-Type header from JSON and see how curl and your browser react.
  3. 3Change the port to come from the PORT environment variable, with 8080 as the fallback.
Official documentation
Check yourselfTake the Project 1 — URL shortener test