8 Gordle as a service
This chapter walks you through turning Gordle, a Wordle-like game, into a Go web service. It begins with the essentials of building and running an HTTP server, clarifying terminology (server, service, endpoints, handlers) and embracing an outside-in strategy: start a listener, add routes, then fill in logic. It reviews REST conventions (resource-oriented paths, HTTP verbs, idempotency) and correct status codes, and shows practical, reproducible testing with curl. You learn how Go’s standard ServeMux (with method+path patterns in recent Go versions) routes requests, and how to return well-formed responses with headers, bodies, and codes.
You implement three endpoints—NewGame, GetStatus, and Guess—by first defining the API contract and JSON structures, then writing handlers that parse inputs from paths, queries, headers, and request bodies. The chapter demonstrates content negotiation basics (Content-Type), proper codes (201 Created, 405 Method Not Allowed), and robust unit testing using httptest plus testify’s assert/require. It explains concurrency at the handler level (each request runs in a goroutine) and the implications for shared state. To keep concerns clean, you introduce domain types (session.Game, GameID, Status, Guess) and an adapter that converts domain objects into API responses, while an internal in-memory repository manages persistence behind a simple interface. You inject that dependency into the router, isolate handler responsibilities, and map domain/storage errors to the right HTTP status codes.
Finally, you integrate the Gordle game logic: read a word corpus, create games, generate secure IDs (e.g., ULID), compute feedback on guesses, record attempts, and transition game status (Playing, Won, Lost). The chapter shows how to embed assets, handle randomness in tests with regular expressions, and guard in-memory state using a mutex. It closes with practical production-minded advice: consider throttling to mitigate overload, add authentication (e.g., token-based) to tie games to users and rate-limit fairly, adopt structured logging (slog) over the basic logger, and return JSON-formatted errors consistently. As an incremental enhancement, you also learn to accept query parameters (like a language selector) to shape behavior without changing routes.
Structure of the service
Target structure of the service
Summary
- A web service is a program that continuously listens to a port and knows what to do with requests based on a set of handlers.
- We can use http.NewServeMux() to create a multiplexer that will route requests, based on the URL they were sent to, to specific handlers. Otherwise, use open-source libraries.
- A handler’s task is to fill an http.ResponseWriter. It should Write to it, and, sometimes, set headers with WriteHeader.
- The default status code set by ResponseWriter.Write is http.StatusOK. If we want to return a different code, we need to call WriteHeader before we start writing the contents of the response via Write.
- If an error happens in an endpoint, the handler should not return this error. If you really want to know what’s gone wrong, a log is the place where the error is last displayed.
- Some directory names have specific meanings in Go. Files inside testdata/ will not be compiled by go build or go run. Files inside internal/ will not be import-able by other modules. vendor/ is a name that should be avoided for historical reasons.
- Types that we define for the API should only be used in the endpoint handlers. The rest of the time, we should be using types of our domain. Domain - or model - types shouldn’t be visible from outside the service’s module. Usually, they hide inside an internal directory.
- A “repository” - oftentimes called “repo” - offers access to the data, which can be stored in a physical database, in memory, or in any form at all.
- It is always a good idea to check whether your code is thread-safe. Write tests and make use of mutexes when necessary.
- The embed package can be used to load the contents of a file (or directory) at compilation time. This is useful, for instance, if you want to keep your SQL queries in .sql files, or when, as we did, you want to load a set of hardcoded values.
- Some, if not most, open-source packages use semantic versioning. Go will natively use the latest v1.x.y version of a package if nothing is specified. In order to enforce using v4.m.n, one should explicitly "go get path/to/package/v4" and use import "path/to/package/v4" in the .go files.
- Regular expressions are a very powerful way to match patterns. You will find yourself using them in various situations and validating randomised values is one of them. As it can be quickly unreadable, do not forget to explain in a comment any regular expressions you write.
- REST API (REpresentational state transfer) or RESTful API is a set of constraints defining an interface between two systems. REST APIs communicate through HTTP and can exchange data through JSON, HTML or even plain text.
- HTTP statuses are useful to communicate precisely what happened on the server side to the client. HTTP status code is a three-digits from 1xx to 5xx, which could mean everything went well like 200, or resources are not found, such as the well-known 404. Returning the proper code means that the client can better understand what happened. When in doubt, return 500. When at a loss, return 418.
- Wildcards can be used to represent path parameters in routes. Registering a handler function with the pattern "/games/{id}" will allow the handle to access the value of the id with request.PathValue("id").
- Several wildcards can be used in the same pattern: "/users/{user_id}/games/{game_id}" could be used to access a specific game for a specific user.
FAQ
What does “Gordle as a service” cover in this chapter?
- Creating and running an HTTP server on a chosen port- Defining REST endpoints and using HTTP verbs (GET, POST, PUT, DELETE)
- Building responses with proper status codes (200, 201, 404, 405, etc.)
- Decoding inputs from path/query parameters, headers, and request bodies (JSON)
- Testing handlers with curl, httptest, and regular expressions
- Designing handlers, routers (ServeMux), and API/domain adapters
- Adding storage with an in-memory repository and making it concurrency-safe with a mutex
- Basic security notions: throttling, authentication, structured logging, and consistent error formats
Note: Containerization and deployment are out of scope.
How do I start a minimal HTTP server in Go and stop it?
- Use net/http:http.ListenAndServe(":8080", handler). Passing nil uses the default handler (returns 404 by default).-
ListenAndServe blocks forever (as a server/daemon should).- Stop it with Ctrl+C in the terminal (sends an interrupt), or kill the process via your OS.
What are servers, services, web services, endpoints, and handlers?
- Server: a computer/process that runs continuously.- Service: an application exposing an API over time (should stay up).
- Web service: a service that communicates over HTTP.
- Endpoint: a path + HTTP method pair (e.g.,
POST /games).- HTTP handler: code that receives an
*http.Request and writes a response.
How should I choose HTTP methods and handle idempotency?
- GET: retrieve a resource (idempotent).- POST: create/process (non-idempotent). Use for operations that change state without a stable resource ID.
- PUT: update/replace a resource (idempotent).
- DELETE: delete a resource (idempotent).
If an endpoint is not idempotent, document it and prefer POST.
How is the NewGame endpoint defined and what status code should it return?
- Route:/games- Method:
POST- Behavior: create a game and return its data/ID as JSON (e.g.,
GameResponse with id, attempts_left, word_length, status, guesses).- Status code:
201 Created (set Content-Type: application/json).- Important: Call
w.WriteHeader(http.StatusCreated) before w.Write(...); writing the body first implicitly sets 200.
How do I route requests with ServeMux and register method-specific handlers?
- Usehttp.NewServeMux() and HandleFunc to register handlers.- With modern Go, register method + path in the pattern (e.g.,
r.HandleFunc(http.MethodPost + " " + "/games", newgame.Handle)).- This avoids manual method checks in the handler and allows reusing the same path for different verbs (e.g.,
POST /games and GET /games).
What are the ways to pass inputs to endpoints (path, query, body, headers)?
- Path parameters: identify a specific resource (e.g.,/games/{id}); read with req.PathValue("id").- Query parameters: optional filters/config in the URL (e.g.,
/games?lang=en); read with r.URL.Query().Get("lang").- Request body: structured payload (JSON) for create/update (e.g.,
PUT /games/{id} with {"guess":"hello"}); decode with json.NewDecoder(r.Body).Decode(&v).- Headers: metadata/auth (e.g., tokens, content negotiation).
How do I test handlers effectively (including path/body and random IDs)?
- Build requests withhttp.NewRequest; set path params via req.SetPathValue("id", "123").- Provide a body with
strings.NewReader(`{"guess":"pocket"}`).- Capture responses with
httptest.NewRecorder() and assert status, headers, and body.- Use testify’s
require (fail-fast) and assert (continue) helpers.- For random IDs, validate format using
regexp (e.g., alphanumeric regex), or normalize the dynamic portion before JSON equality checks.
How is state stored and made concurrency-safe?
- Use an in-memory repository:map[GameID]Game with methods like Add, Find, Update.- Protect map access with a
sync.Mutex inside the repository (lock/unlock in each method).- Initialize the repo at startup and inject it into handlers via factory functions that return
http.HandlerFunc.- Depend on minimal interfaces (e.g.,
gameAdder, finder/updater) so tests can stub the repo easily.
How should I handle errors and what security improvements are recommended?
- Map domain/storage errors to HTTP codes: e.g., not found → 404, invalid guess → 400, game over → 403, unknown → 500.- Avoid leaking internals: log detailed errors, return sanitized messages; prefer JSON error bodies like
{"error":"game over"}.- Security tips: rate limiting/throttling (to mitigate DDoS), authenticate users/apps (e.g., OAuth 2.0 tokens in headers), and use structured logging (
slog instead of log).- Consider embedding assets (via
embed) and loading configuration at startup; validate inputs consistently.
Learn Go with Pocket-Sized Projects ebook for free