What a REST Service Is Made Of

One process, three listening sockets, a router, a handful of layers and two database connections: the whole shape of a Go REST service in one picture, with the vocabulary to talk about it.


A REST service is one operating system process. It starts, reads its configuration, opens some sockets and some database connections, then accepts connections until something tells it to stop. Everything else in this guide is a detail of that sentence.

From the outside it is a set of URLs that return JSON, which says little about the thing you have to deploy, watch and debug at an inconvenient hour.


A service at runtime

It holds listening sockets. A socket bound to a port is an operating system resource the process owns for its whole life. Nothing reaches the code until one is bound, and a bound socket is reachable from wherever the network says it is, whatever the code intended. That is why this service has three, per page 17.

It serves many requests at once. Go’s HTTP server runs each connection in its own goroutine, so two requests inside the same function at the same time is the normal case. Anything a handler can reach that was not created per request is shared, and shared means it has to be safe to use concurrently. Page 02 has the practice.

It outlives its requests. A connection pool, an HTTP client and a cache are created once at startup and borrowed by requests that last milliseconds. Sizing, health-checking and closing them is where a service usually falls over under load. Pages 11 and 12.

Something is watching it. An orchestrator, a systemd unit or a supervisor decides whether the process is healthy, sends traffic to it, and restarts it, communicating by calling HTTP endpoints on a schedule it owns and by sending signals. The service’s side of that conversation is pages 13 and 19.


The picture

System design diagram: callers and a generic edge above the service process, which holds three listeners, their middleware chains, handlers, a service layer, a repository and outbound clients, over the databases and upstream services. Each part is described below.

Read it top to bottom: callers at the top, a request working down through the layers, dependencies at the bottom. The box labelled “service process” is the program you are building, and what sits above and below it usually belongs to somebody else. The sections below name each part in the order the request meets it.

The edge

Between a public client and the public listener sits a box marked “edge”: an ingress, an API gateway, a CDN, a load balancer, a mesh sidecar, or several stacked. It is drawn unlabelled because the product does not matter and the boundary does. Rate limiting, TLS termination, compression, request size caps and sometimes authentication commonly run there already, and writing them again in Go is often wasted work. Page 08 takes each in turn.

The other two callers bypass the edge: a cluster caller on the private listener is another service inside the same trust zone, and an operator or metrics scrape on the monitoring listener is infrastructure. Page 17 is about what has to be true for that to be safe.

The three listeners

The service binds three ports, and each has a different audience.

:8073 is public. Whatever the product exposes to the outside world lands here, and this is the traffic to assume is hostile.

:8072 is private: administrative operations, internal lookups, and anything else that would be a liability on the public port.

:8071 is monitoring: health, metrics, profiling, diagnostics, and nothing that serves a product function.

The split is enforcement rather than a naming convention. A route is registered against one server, so /metrics on the monitoring server is unreachable on the public port by any request, including a malformed one and one that arrives after somebody reorders the middleware chain. Three sockets cost three lines of configuration. Page 07 builds them, and page 17 argues the boundary.

Router and middleware chain

A router maps an incoming method and path onto a function. Middleware wraps that function, seeing the request on the way in and the response on the way out. Each listener has its own router and its own chain.

The chain carries the cross-cutting work: reading or generating a trace ID, recording request duration, catching a panic before it kills the connection, logging what happened. Order matters, and page 08 has the rules. The three chains are separate objects, so the public one can carry rate limiting the monitoring one does not need.

Handlers

A handler runs for one route: read the request, hand the work to something that knows the business rules, turn the answer into a response. No SQL, no retry loops, no policy about what an item is.

Public and private handlers are separate boxes because they are separate Go packages, internal/httphandlerpub and internal/httphandlerpriv. They can share a service layer underneath while exposing different route sets above. Pages 14 to 16 build real ones.

The operational routes

The monitoring listener’s routes come with the server rather than being written by hand:

RouteWhat it answers
/An index of the routes this server exposes
/ipThe public IP address this instance sees itself as
/metricsThe Prometheus scrape endpoint
/pingIs this process alive and serving
/pprof/*Go’s profiling endpoints
/statusAre this process’s dependencies healthy

/ping and /status answer different questions, and pointing the wrong probe at the wrong one produces either a restart loop or a service that receives traffic before its database is reachable, per page 13. /metrics and /pprof must never appear on a public port. The private and public servers enable only /ping.

The service layer

Business rules: what an item is, what a name may contain, how large a page may be. This layer knows nothing about HTTP, taking Go values and returning Go values or errors, which makes it testable without a server and reusable from a queue consumer later.

The reference implementation ships one, internal/item, behind four public endpoints. It is small on purpose and it is real: Part V reads it line by line and then covers what it leaves out.

The repository and the two databases

The repository turns Go values into SQL and back, and is the only place in the service that knows a column name.

There are two connections. The main connection carries writes and the reads that have to see them. The read connection points at a replica, carries queries that tolerate replication lag, and is sized independently. A service with no replica points both at the same database and loses nothing, and keeping them separate from the start makes the day a replica appears a configuration change.

Each connection is a pool. Page 12 covers the difference and the settings that decide whether a slow query becomes a queue.

Outbound clients

Calls the service makes to other services. Each needs a timeout, structured logging of what went out and came back, metrics, and the trace ID forwarded. Most need a retry policy, which is more dangerous than it looks: retrying into a struggling upstream is how a slow dependency becomes a dead one. Page 11.

The reference service has one, a client for ipify behind the /ip route, and it is excluded from the health checks on purpose. A service that reports itself unhealthy when a third-party address lookup is down has made its availability depend on something it does not need.

The cross-cutting parts

Configuration, the logger, the metrics client, the health checks and the shutdown machinery are drawn as a legend rather than a box in the flow, because they sit at no single point in the request path. They are created once during startup and handed to everything that needs them.

That happens in one function, bind, in internal/cli/bind.go, which holds the entire dependency graph in one readable place and is the file to open first in a service you have never seen. Page 03 walks the path that reaches it, page 05 covers the lifecycle objects it threads through everything, and page 20 is about keeping it readable as the service grows.


What this guide covers, and what it does not

The table of contents lists what is covered. What is not:

  • Authentication and authorisation as a subject. Page 17 covers the access control a separate listening socket can enforce, and where an authentication middleware attaches. Issuing and verifying credentials outgrows a page. Starting points: the JWT article, the password hashing article, and nurago.org’s security notes.
  • gRPC, GraphQL, WebSockets, server-sent events. Different protocols with different lifecycles.
  • Email, OAuth flows, file uploads, payment integration. Product features rather than service infrastructure.
  • Choosing a database. The examples are MySQL through database/sql, and everything except the DSN and the migration syntax applies to PostgreSQL.

The toolkit is substitutable

The guide builds with nurago, a set of Go packages covering the infrastructure concerns above, and every page starts from a problem before reaching for one. Where the standard library already solves something, the guide says so before showing the wrapper: net/http serves HTTP perfectly well, and the httpserver package on top of it is startup validation, a route binder, operational endpoints and shutdown choreography.

Where a reasonable engineer would pick a different tool, the guide names it: chi or echo, zerolog or zap used directly, sqlc or an ORM, a service mesh instead of an in-process retrier. The test to apply as you read is whether the page still tells you how to build the service if you never install any of it.


Following along

The reference implementation is a complete, deployable service carrying one small feature, the /items endpoints Part V reads. Clone the repository, edit project.cfg in the root, and run make project: it copies examples/service into target/<cvspath>/<name>/ and substitutes your project’s name, description, author and repository path through every filename and every file in the tree. This guide used these values:

NURAGOEXAMPLE=INVENTORYSVC
nuragoexample=inventorysvc
nuragoexampleshortdesc=Inventory service
nuragoexamplelongdesc=Tracks named items and the quantity held of each
nuragoexampleauthor=Example Author
nuragoexampleemail=author@example.com
nuragoexamplecvspath=github.com/example
nuragoexampleprojectlink=https://github.com/example/inventorysvc
nuragoexampleowner=example
nuragoexamplevcsgit=https://github.com/example/inventorysvc.git

The names on the left are the template’s placeholder tokens and stay as they are; only the values change. The result is a module at github.com/example/inventorysvc, the import path in every Go snippet in Part V.

target/ is nurago’s build directory: Git ignores it and make clean deletes it. Move the generated tree out of the clone before working in it. make project drops the replace directive from the generated go.mod, so the project resolves nurago from the module proxy and needs nothing from the checkout. git init it and point the remote at the repository named in project.cfg.

mv target/github.com/example/inventorysvc ../inventorysvc
cd ../inventorysvc
make deps
go mod tidy
make test
make build

make build writes the binary to target/usr/bin/inventorysvc. Start it from the project root:

./target/usr/bin/inventorysvc

It logs its startup to stdout, one JSON object per line, and stays in the foreground until Ctrl-C. No config file is needed: the built-in defaults bind the three ports and leave the database disabled. --configDir resources/etc/inventorysvc points it at the file the template ships, and page 04 covers the search order.

From another shell:

curl localhost:8071/status
curl localhost:8071/ping
curl localhost:8073/uid
curl localhost:8073/items

The first two answer from the monitoring listener and the rest from the public one, and curl localhost:8071/ lists the operational routes above.

/items is a 404, which is the interesting one. The item endpoints need the database, the default configuration leaves it disabled, and the routes are therefore never registered rather than registered against a handler that can only fail. make localenvup brings up the service against a migrated MySQL in containers, which is where page 14 picks them up.


Next: The Standard Library Underneath All of This.