Stage 1 · Code
Building Web Services
Building JSON APIs
Turn Go structs into a small, well-behaved web API: decode requests, validate input, return useful status codes, and speak JSON clearly in both success and failure cases.
Why JSON Needs Structure
JSON is just a text format. That sounds friendly, but it can fool beginners into thinking the server should simply accept whatever text shows up and hope for the best. Good APIs are stricter than that. They decode JSON into a known Go shape, validate that the important fields are present and sensible, and then answer with a response that is equally deliberate.
Think of a JSON API like a shipping counter with a printed form. The form is not there to be bureaucratic. It exists because both sides need the same structure. If the sender writes a phone number in the street-address box, the shipment may still be text on paper, but it is the wrong text in the wrong place. Servers need the same discipline.
Do not let raw request data leak deep into your program. First decode the JSON into a Go struct. Then validate it. Only then should the handler create records, call services, or touch storage.
Building a Small API
We'll build two endpoints for a tiny deployment queue API: POST /deployments creates a deployment record, and GET /deployments/{id} fetches one back. The storage is just an in-memory map because the lesson is about HTTP behavior, not databases.
Notice that the JSON encoder and decoder are doing translation work, not decision-making work. json.Decoder can tell you whether the body is valid JSON. It cannot decide whether an empty service field makes sense for your API. That is why decoding and validation are separate steps.
Status Codes that Mean Something
A lazy API answers everything with 200 OK and hides the real outcome inside the body. A useful API lets the status code carry part of the meaning. Clients can react faster and more safely when the HTTP layer itself tells the truth.
| Status | When to use it | Example in this lesson |
|---|---|---|
200 OK | The request succeeded and you are returning the existing resource or result | Fetching GET /deployments/dep-001 |
201 Created | A new resource was created | Creating a deployment with POST /deployments |
400 Bad Request | The client sent malformed or invalid input | Missing service or unsupported environment |
404 Not Found | The requested resource does not exist | Fetching dep-999 from an empty map |
500 Internal Server Error | The server failed while trying to process a valid request | A database write or encoding step blows up unexpectedly |
If the client sent bad JSON, that is a 400 problem. If your server crashed while handling perfectly valid JSON, that is a 500 problem. Mixing those together makes debugging much harder for everyone.
Tracing Success and Validation Failure
Let's run the same endpoint through two different outcomes so the flow becomes concrete instead of abstract.
POST /deployments HTTP/1.1
Host: localhost:8080
Content-Type: application/json
{
"service": "billing-api",
"version": "1.4.2",
"environment": "staging"
}This request has valid JSON and all required fields. The environment value is one the handler accepts.
HTTP/1.1 201 Created
Content-Type: application/json
{
"id": "dep-001",
"service": "billing-api",
"version": "1.4.2",
"environment": "staging",
"status": "queued"
}The server generated the ID, preserved the validated fields, added its own status, and used 201 Created to say a new resource now exists.
POST /deployments HTTP/1.1
Host: localhost:8080
Content-Type: application/json
{
"service": "",
"version": "1.4.2",
"environment": "preview"
}One of the biggest API design habits to build early is honesty. A handler should say clearly whether the request was malformed, invalid, missing a resource, or blocked by a server-side failure.
Quiz and Practice
Why do we validate after decoding instead of treating successful JSON decoding as enough?
Hands-On Project
Implement `validateCreateDeployment`. It must reject empty `Service`, reject empty `Version`, and allow only `staging` or `production` for `Environment`. Return `nil` when the input is valid.
Summary and Key Takeaways
- JSON is only a transport format; your API still needs an explicit Go shape and explicit validation rules.
- A clean handler usually follows the order: decode, validate, act, encode.
- Set
Content-Typedeliberately so clients know they are receiving JSON. - Use status codes to tell the truth about the outcome instead of hiding everything behind
200 OK. - Success and error responses are both part of the API contract, so both deserve thoughtful design.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.