142 lines
9.2 KiB
Markdown
142 lines
9.2 KiB
Markdown
# Endpoints
|
|
|
|
| Method | URL Pattern | Handler | Action |
|
|
|--------|-----------------|---------------------|--------------------------------------|
|
|
| GET | /v1/healthcheck | healthCheckHandler | Show application information |
|
|
| GET | /v1/movies | listMoviesHandler | Show the details of all movies |
|
|
| POST | /v1/movies | createMoviesHandler | Create a new movie |
|
|
| GET | /v1/movies/:id | showMovieHandler | Show the details of a specific movie |
|
|
| PUT | /v1/movies/:id | editMovieHandler | Edit the details of a specific movie |
|
|
| DELETE | /v1/movies/:id | deleteMovieHandler | Delete a specific movie |
|
|
|
|
# Installation
|
|
|
|
## Launch API
|
|
`go run ./cmd/api`
|
|
|
|
If you want, you can also verify that the command-line flags are working correctly by specifying alternative **port** and **env** values when starting the application.
|
|
When you do this, you should see the contents of the log message change accordingly. For example :
|
|
|
|
`go run ./cmd/api -port=3030 -env=production`
|
|
**time=2025-10-10T11:08:00.000+02:00 level=INFO msg=
|
|
"starting server" addr=:3030 env=production**
|
|
|
|
## Test endpoints
|
|
`curl -i localhost:4000/v1/healthcheck`
|
|
The *-i* flag in the command above instructs curl to display the HTTP response headers as well as the response body.
|
|
|
|
### Result
|
|
|
|
HTTP/1.1 200 OK
|
|
Date: Mon, 05 Apr 2021 17:46:14 GMT
|
|
Content-Length: 58
|
|
Content-Type: text/plain; charset=utf-8
|
|
|
|
status: available
|
|
environment: development
|
|
version: 1.0.0
|
|
|
|
## API Versioning
|
|
|
|
There are two comon approaches to doing this :
|
|
1. By prefixing all URLs with your API version, like **/v1/healthcheck** or **/v2/healthcheck**
|
|
2. By using custom **Accept** and **Content-Type** headers on requests and responses to convey the API version, like **Accept: application/vnd.greenlight-v1**
|
|
|
|
From an HTTP semantics point of view, using headers to convey the API version is the 'purer' approach. But from a user-experience point of view, using a URL prefix is arguably better. It makes it possible for developers to see which version of the API is being used at a glance, and it also means that the API can still be explored using a regular web browser (which is harder if custom headers are required).
|
|
|
|
## Additional Information
|
|
|
|
### How different Go Types are encoded
|
|
|
|
The following table summarizes how different Go types are mapped to JSON data types during encoding :
|
|
|
|
| Go type | JSON type |
|
|
|---------------------------------------------------|----------------------------|
|
|
| bool | JSON boolean |
|
|
| string | JSON string |
|
|
| int*, uint*, float*, rune | JSON number |
|
|
| array, slice | JSON array |
|
|
| struct, map | JSON object |
|
|
| nil pointers, interface values, slices, maps, etc | JSON null |
|
|
| chan, func, complex* | Not supported |
|
|
| time.Time | RFC3339-format JSON string |
|
|
| []byte | Base64-encoded JSON string |
|
|
|
|
The last two of these are special cases which deserve a bit more explanation :
|
|
|
|
- Go **time.Time** values (which are actually a struct behind the scenes) will be encoded as a JSON string in RFC 3339 format like **"2020-11-08T06:27:59+01:00"**, rather than as a JSON object.
|
|
- A **[]byte** slice will be encoded as a base64-encoded JSON string, rather than as a JSON array. So, for example, a byte slice of **[]byte{'h','e','l','l','o'}** would appear as **"aGVsbG8="** in the JSON output. The base64 encoding uses padding and the standard character set.
|
|
|
|
A few other important things to mention :
|
|
|
|
- Encoding of nested objects is supported. So, for example, if you have a slice of structs in Go that will encode to an *array of objects* in JSON.
|
|
- Channels, functions and **complex** number types cannot be encoded. If you try to do so, you'll get a **json.UnsupportedTypeError** error at runtime.
|
|
- Any pointer values will encode as *the value pointed to*.
|
|
|
|
### Enveloping responses
|
|
|
|
The data of the endpoint /v1/movies/123 is nested under the key "movie", rather than being the top-level JSON object itself.
|
|
Enveloping response data like this isn't strictly necessary, and whether you choose to do so is partly a matter of style and taste. But there are a few tangible benefits :
|
|
|
|
1. Including a key name (like "movie") at the top-level of the JSON helps make the response more self-documenting. For any humans who see the response out of context, it is a bit easier to understand what the data relates to.
|
|
2. It reduces the risk of errors on the client side, because it's harder to accidentally process one response thinking that it is something different. To get at the data, a client must explicitly reference it via the "movie" key.
|
|
3. If we always envelope the data returned by our API, then we mitigate a security vulnerability in older browsers which can arise if you return a JSON array as a response.
|
|
|
|
### Advanced JSON Customization
|
|
|
|
_When Go is encoding a particular type to JSON, it looks to see if the type has a **MarshalJSON()** method implemented on it. If it has, then Go will call this method to determine how to encode it._
|
|
|
|
Strictly speaking, when Go is encoding a particular type to JSON it looks to see if the type satisfies the json.Marshaler interface, which looks like this :
|
|
|
|
`type Marshaler interface {
|
|
MarshalJSON() ([]byte, error)
|
|
}`
|
|
|
|
If the type does satisfy the interface, then Go will call its **MarshalJSON()** method and use the []byte slice that it returns as the encoded JSON value.
|
|
|
|
If the type doesn't have a **MarshalJSON()** method, then Go will fall back to trying to encode it to JSON based on its own internal set of rules.
|
|
|
|
So, if we want to customize how something is encoded, all we need to do is implement a **MarshalJSON()** method on it which returns a _custom JSON representation of itself_ in a **[]byte** slice.
|
|
|
|
An example is available here : **internal/data/runtime.go**
|
|
|
|
### System-generated error responses
|
|
|
|
In certain scenarios Go's **http.Server** may still automatically generate and send plain-text HTTP responses. These scenarios include when :
|
|
|
|
- The HTTP request specifies an unsupported HTTP protocol version.
|
|
- The HTTP request contains a missing or invalid **Host** header, or multiple **Host** headers.
|
|
- The HTTP request contains an empty **Content-Length** header.
|
|
- The HTTP request contains an unsupported **Transfer-Encoding** header.
|
|
- The size of the HTTP request headers exceeds the server's **MaxHeaderBytes** setting.
|
|
- The client makes a HTTP request to an HTTPS server.
|
|
|
|
For example, if we try sending a request with an invalid **Host** header value, we will get a response like this:
|
|
|
|
`$ curl -i -H "Host: こんにちは" http://localhost:4000/v1/healthcheck
|
|
HTTP/1.1 400 Bad Request: malformed Host header
|
|
Content-Type: text/plain; charset=utf-8
|
|
Connection: close
|
|
|
|
400 Bad Request: malformed Host header`
|
|
|
|
Unfortunately, these responses are hard-coded into the Go Standard library, and there's nothing we can do to customize them to use JSON instead.
|
|
|
|
But while this is something to be aware of, it's not necessarily something to worry about. In a production environment it's relatively unlikely that well-behaved, non-malicious, clients would trigger these responses anyway, and we shouldn't be overly concerned if bad clients are sometimes set a plain-text response instead of JSON.
|
|
|
|
### Panic recovery in other goroutines
|
|
|
|
It's important to realize that our middleware will only recover panics that happen in the _same goroutine that executed the **recoverPanic()** middleware_.
|
|
|
|
If, for example, you have a handler which spins up another goroutine (e.g. to do some background processing), then any panics that happen in the background goroutine will not be recovered - not by the **recoverPanic()** middleware... and not by the panic recovery build into **http.Server**. These panics will cause your application to exit and bring down the server.
|
|
|
|
So, if you are spinning up additional goroutines from within your handlers and there is any chance of a panic, you **must make sure** that you recover any panics from within those goroutines too.
|
|
|
|
A demonstration will follow when we will use a background goroutine to send welcome emails to our API users.
|
|
|
|
### Performance
|
|
|
|
json.MarshalIndent() takes 65% longer to run and uses around 30% more memory than json.Marshal(), as well as making two more heap allocations. Those figures will change depending on what you're encoding, but they're fairly indicative of the performance impact.
|
|
|
|
For most applications this performance difference simply isn't something that you need to worry about. In real terms, we're talking about a few thousandths of a millisecond - and the improved readability of responses is probably worth this trade-off.
|
|
But if your API is operating in a very resource-constrained environment, or needs to manage extremely high levels of traffic, then this is worth being aware of, and you may prefer to stick with using json.Marshal() instead. |