JavaRush /Courses /Go SELF /API documentation — curl examples, errors, auth header

API documentation — curl examples, errors, auth header

Go SELF
Level 64, Lesson 4
Available

1. API documentation as part of the contract

If tests are a way to prove to a computer that the API works as intended, documentation is a way to prove it to a human. And not only to an API user, but also to future you two weeks from now, when you forget why POST /done returns 204 rather than 200. Documentation is not a three-volume novel; it is a short, strict “agreement”: what endpoints exist, which headers are required, which responses are considered normal, and what errors look like.

Most often, documentation lives in the project’s README.md, sometimes in a separate file like docs/api.md. For a learning project, this is more than enough: we are not building a 40-page developer portal, we are building a clear contract that can be verified by hand via curl and that matches what the tests verify.

To avoid confusion in terms, let’s agree on a simple principle right away: if a behavior matters to the client, it must be described. If it matters only to the server (internal details), it must not “leak” outward—neither into the documentation nor into the responses.

Minimum documentation skeleton for our Task API

When you open documentation, the reader’s brain tries to answer three questions in the first 10 seconds: “where do I call?”, “how do I authenticate?”, and “what do I get back?”. If those answers are buried in the middle, assume the reader has already walked away to make tea (and tea won).

Below is a simple skeleton that works well for almost any JSON API, including our learning task service. I’ll show it as a “template” you can keep in README.md.

Header: base URL, version, format

Let’s assume that locally the service listens on http://localhost:8080.

Parameter Value
Base URL
http://localhost:8080
API prefix
/api/v1
Data format JSON (UTF-8)
Error format JSON error envelope (see below)
Auth header X-API-Key: <key>

Important: even if you are only running the service locally for now, it is still useful to write the base URL explicitly. It cuts the number of “why am I getting 404?” questions by about half.

2. curl examples that actually work

curl is like a Swiss Army knife: you can slice a salad, fix a bike, and accidentally cut off a finger if you mix up the method and the URL. In documentation, it is valuable because it provides a reproducible scenario. The reader can copy the command and see the same result (if the conditions are met: the server is up, the key is correct, the routes match).

Most often, examples need four things: method, URL, headers, and request body. In practice, it looks like this:

curl -i \
  -X POST 'http://localhost:8080/api/v1/tasks' \
  -H 'Content-Type: application/json' \
  -H 'X-API-Key: dev-secret' \
  -d '{"title":"Buy milk"}'

Here -i asks to show the status and response headers (this is convenient because documentation is not only the JSON body, but also the status code). We set the Content-Type header so the server clearly understands it received JSON, not “just text, but we feel like it’s JSON”.

One more important point: in documentation it’s better to show short commands. If a command grows to 15 lines with tricky substitutions, the reader starts to suspect it is a magical demon-summoning ritual rather than an API request.

4. Authorization: one header, one meaning, zero surprises

Authorization in learning APIs is often “bolted on from the side” and turns into chaos: sometimes you need Authorization, sometimes a cookie, sometimes a ?token=... parameter, sometimes “today without a key because I’m testing”. This quickly kills documentation, testability, and common sense.

We lock in a simple contract: every request to /api/v1/... requires an API key in a header:

  • header: X-API-Key: <key>
  • if the key is missing or invalid, the client receives an error in our unified format

Example request without a key (we expect an error, and that’s normal):

curl -i 'http://localhost:8080/api/v1/tasks'

Example request with a key:

curl -i \
  -H 'X-API-Key: dev-secret' \
  'http://localhost:8080/api/v1/tasks'

The important philosophical part (yes, APIs can have philosophy): outwardly we return a clear message, but we do not reveal more than necessary. The general idea “don’t expose internal details as a contract” is useful when designing errors: if you expose internals, you accidentally promise the client that “it will always be like this”, and that is a dangerous promise.

5. Our API endpoints: table + curl examples

To make documentation scannable, it’s convenient to show an endpoint table first and then examples. The table is the “map”, and the examples are “how to walk the road without falling into a ditch”.

Endpoint table

Method Path Purpose Success
GET
/health
service liveness check
204 No Content
POST
/api/v1/tasks
create a task
201 Created
+ task JSON
GET
/api/v1/tasks
list tasks (with filters)
200 OK
+ JSON array
GET
/api/v1/tasks/{id}
get a task by id
200 OK
+ task JSON
POST
/api/v1/tasks/{id}/done
mark a task as done
204 No Content
DELETE
/api/v1/tasks/{id}
delete a task
204 No Content

Notice that we deliberately write {id} in curly braces rather than “42”, so it’s clear this is a path parameter. The routing tests you wrote using ServeMux patterns should confirm that {id} is actually extracted and handled.

6. curl examples: “happy path” scenarios

Health check

Health is a useful thing for monitoring and for “did I even start the server, or am I talking to myself?”.

curl -i 'http://localhost:8080/health'
# HTTP/1.1 204 No Content

There is intentionally no body here: 204 means “everything is fine, but there is no content”.

Creating a task

Create a task Buy milk. Consider this “Hello, world” for a todo service.

curl -i \
  -X POST 'http://localhost:8080/api/v1/tasks' \
  -H 'Content-Type: application/json' \
  -H 'X-API-Key: dev-secret' \
  -d '{"title":"Buy milk"}'

An example of the expected response (you can adapt the format to your model; the main thing is that it stays stable):

{
  "id": 1,
  "title": "Buy milk",
  "done": false
}

What matters here is not the specific number 1, but that the client understands the structure: there is id, title, done. If you add fields later, that should also be reflected in the documentation.

Getting the task list

curl -i \
  -H 'X-API-Key: dev-secret' \
  'http://localhost:8080/api/v1/tasks'

Response:

[
  { "id": 1, "title": "Buy milk", "done": false }
]

If you support filters (for example, done or contains), the documentation should show at least one example so the client does not have to guess “what was intended”:

curl -i \
  -H 'X-API-Key: dev-secret' \
  'http://localhost:8080/api/v1/tasks?done=false'

Getting a task by id

curl -i \
  -H 'X-API-Key: dev-secret' \
  'http://localhost:8080/api/v1/tasks/1'

Response:

{ "id": 1, "title": "Buy milk", "done": false }

Mark a task as done

We use a separate endpoint /done so the action reads like a “command” (and so the client doesn’t agonize over whether it should be PATCH or POST). We return 204 because often it’s enough for the client to know the operation succeeded.

curl -i \
  -X POST \
  -H 'X-API-Key: dev-secret' \
  'http://localhost:8080/api/v1/tasks/1/done'
# HTTP/1.1 204 No Content

Deleting a task

Deletion is convenient to make semantically idempotent: if the task is already gone, you can return 404, or you can agree to still return 204. But whatever choice you make, it must be described and verified by tests.

Let’s say our agreement is: if the task doesn’t exist, return 404. Then a “normal delete” looks like this:

curl -i \
  -X DELETE \
  -H 'X-API-Key: dev-secret' \
  'http://localhost:8080/api/v1/tasks/1'
# HTTP/1.1 204 No Content

7. Errors as part of the API: a unified error envelope and a status table

Errors are not a “crash that’s embarrassing to show”—they are a normal execution branch. The client must be able to handle “not found”, “invalid”, “no access”, “something failed internally”. That’s why we fix a single response format so the client can parse errors the same way for all endpoints, without playing a guessing game.

Format (reminder: our contract):

{
  "error": {
    "code": "validation",
    "message": "invalid request",
    "fields": { "title": "must not be empty" }
  }
}

The fields field is present only for validation errors. In other cases it is either absent or empty.

Table: status → error.code → meaning

HTTP status error.code When it happens
400 Bad Request
validation
invalid input data (including id not being a number)
401 Unauthorized
unauthorized
missing X-API-Key
403 Forbidden
forbidden
the key is present, but not accepted (if you distinguish 401/403)
404 Not Found
not_found
resource not found
500 Internal Server Error
internal
server-side error; we do not expose details

A key security principle: for 500-class errors, the message for the client must be stable and safe, and the details must go to logs. This approach is a classic in the Go world: a clear message and a correct status for the user; detailed diagnostics separately for the developer.

One more important thought: if in one place you return {"error":"...string..."}, and in another {"message":"..."}, the client starts writing a “zoo of parsers”, and then begins to hate you (and a little bit themselves). That’s why we stick to a single envelope.

8. curl error examples and why you should document them

Validation error (400)

Try to create a task with an empty title:

curl -i \
  -X POST 'http://localhost:8080/api/v1/tasks' \
  -H 'Content-Type: application/json' \
  -H 'X-API-Key: dev-secret' \
  -d '{"title":""}'

We expect:

{
  "error": {
    "code": "validation",
    "message": "invalid request",
    "fields": { "title": "must not be empty" }
  }
}

This matters in documentation for two reasons. First, the client understands what to do (fix the field). Second, the client understands what exactly to parse: fields.title, rather than “extract text with a regex from message” (regexes in responses are a special kind of suffering).

Not found (404)

Request a task that doesn’t exist:

curl -i \
  -H 'X-API-Key: dev-secret' \
  'http://localhost:8080/api/v1/tasks/999'

Response:

{
  "error": {
    "code": "not_found",
    "message": "task not found"
  }
}

Authorization error (401/403)

Request without a key:

curl -i 'http://localhost:8080/api/v1/tasks/1'

Response (example):

{
  "error": {
    "code": "unauthorized",
    "message": "missing api key"
  }
}

If you distinguish between “missing key” and “invalid key”, then in the second case:

curl -i \
  -H 'X-API-Key: wrong-key' \
  'http://localhost:8080/api/v1/tasks/1'

Response (example):

{
  "error": {
    "code": "forbidden",
    "message": "invalid api key"
  }
}

What matters here is not so much “what exactly to call it”, but that you lock in the behavior and then don’t change it accidentally.

Internal error (500): why you must not “show err.Error()” to the client

A 500 error in a good API looks boring. And that’s a compliment. It should be as stable in shape as possible:

{
  "error": {
    "code": "internal",
    "message": "internal error"
  }
}

If instead you return the real cause such as "dial tcp: connection refused", the client starts depending on the text, and you accidentally expose details you do not want to promise publicly. The recommendation “don’t expose internal details as a contract” is very practical here: what is an internal implementation detail must not become a public promise.

9. Useful nuances: X-Request-ID and “living” documentation

X-Request-ID in documentation

Sometimes it feels like request id is “something for large microservices”, while our learning server is “small anyway”. In practice, X-Request-ID is a super-useful thing even in a small service: it makes it easy to find a request in logs, and in tests you can verify that middleware works.

In documentation, one example is enough: “how to pass a request id” and “that the server will return it back”.

curl -i \
  -H 'X-API-Key: dev-secret' \
  -H 'X-Request-ID: test-123' \
  'http://localhost:8080/api/v1/tasks'

And you expect the response headers to include:

X-Request-ID: test-123

From the client’s perspective, this is transparent and useful functionality; from the server’s perspective, it is discipline that saves hours of debugging.

How to keep documentation “alive”

Documentation usually breaks not because people are bad, but because “we tweaked the handler, tests are green, and README was forgotten”. So the most practical approach is to tie documentation to what is already protected by tests.

When you change API behavior (status code, a field in JSON, error format), you have three “signals”: integration tests, unit tests, and common sense. If tests truly verify the contract, changing the contract almost always forces changes to tests. And since you already went to update the tests, it’s a good moment to update the curl example in the README as well, because it is essentially the same scenario, just for a human.

And yes, it’s normal that documentation sometimes lags a bit. What’s not normal is when it lags forever and turns into API fan fiction.

10. Common mistakes when documenting an API

Mistake #1: documentation describes “how we implemented it right now” instead of “how the client should use it”.
It’s very easy to drift into implementation details: “we have ServeMux there, then writeJSON, then parseID…”. This does not help the client. The client needs methods, paths, headers, JSON, statuses, and errors. Everything else is your internal adventure.

Mistake #2: curl examples cannot be run as-is.
The most painful kind of bad documentation: the commands look nice, but they are missing the base URL, missing the required auth header, or forgot Content-Type. The reader copies it, gets 401/415/400, and concludes the API is broken (and sometimes that they themselves are broken, which is even sadder).

Mistake #3: errors are described in words, but not in structure.
The phrase “in case of an error, the server returns JSON with a message” is almost useless. Useful is: which HTTP status, which error.code, which error.message, and when fields appears. If the client is writing an error handler, they must understand what to parse.

Mistake #4: leaking internal errors in 500 responses.
When the server returns err.Error() to the outside world for an internal error, it may seem convenient (“at least it’s clear!”), but it turns internals into a public contract and may expose unnecessary information. It’s better to keep a fixed message externally and put details in logs.

Mistake #5: no authorization example, or “floating” authorization.
If today you need X-API-Key, tomorrow “it’s optional”, and the day after “only if the Moon is in its third phase”, the client application quickly turns into a pile of hacks. One header, one meaning, one example—and everyone is calmer.

1
Task
Go SELF, level 64, lesson 4
Locked
API contract header
API contract header
1
Task
Go SELF, level 64, lesson 4
Locked
Curl scenarios
Curl scenarios
1
Task
Go SELF, level 64, lesson 4
Locked
Error Guide
Error Guide
1
Task
Go SELF, level 64, lesson 4
Locked
Creation contract
Creation contract
1
Survey/quiz
HTTP Testing, 64 level, 4 lesson
Unavailable
HTTP Testing
HTTP Testing
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION