API Endpoints and RESTful Routing
Over the next few sections of this book we’re going to gradually build up our API so that the endpoints start to look like this:
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 | createMovieHandler | Create a new movie |
GET | /v1/movies/:id | showMovieHandler | Show the details of a specific movie |
PUT | /v1/movies/:id | editMovieHandler | Update the details of a specific movie |
DELETE | /v1/movies/:id | deleteMovieHandler | Delete a specific movie |
If you’ve built APIs with REST style endpoints before, then the table above probably looks very familiar to you and doesn’t require much explanation. But if you’re new to this, then there are a couple of important things to point out.
The first thing is that requests with the same URL pattern will be routed to different handlers based on the HTTP request method. For both security and semantic correctness, it’s important that we use the appropriate HTTP method for the action that the handler is performing.
In summary:
Method | Usage |
---|---|
GET | Use for actions that retrieve information only and don’t change the state of your application or any data. |
POST | Use for non-idempotent actions that modify state. In the context of a REST API, POST is generally used for actions that create a new resource. |
PUT | Use for idempotent actions that modify the state of a resource at a specific URL. In the context of a REST API, PUT is generally used for actions that replace or update an existing resource. |
PATCH | Use for actions that partially update a resource at a specific URL. It’s OK for the action to be either idempotent or non-idempotent. |
DELETE | Use for actions that delete a resource at a specific URL. |
The other important thing to point out is that our API endpoints will use clean URLs, with wildcard parameters interpolated in the URL path. So — for example — to retrieve the details of a specific movie a client will make a request like GET /v1/movies/1
, instead of appending the movie ID in a query string parameter like GET /v1/movies?id=1
.
Choosing a router
In this book we’re going to use the popular third-party package httprouter
as the router for our application, instead of using http.ServeMux
from the standard-library.
There are two reasons for this:
We want our API to consistently send JSON responses wherever possible. Unfortunately,
http.ServeMux
sends plaintext (non-JSON)404
and405
responses when a matching route cannot be found, and it’s not possible to easily customize these without causing a knock-on effect that inhibits the automatic sending of405
responses. There is an open proposal to improve this in future versions of Go, but for now it’s a pretty significant drawback.Additionally — and less importantly for most applications —
http.ServeMux
does not automatically handleOPTIONS
requests.
Both of these things are supported by httprouter
, along with providing all the other functionality that we need. The package itself is stable and well-tested, and as a bonus it’s also extremely fast thanks to its use of a radix tree for URL matching. If you’re building a REST API for public consumption, then httprouter
is a solid choice.
If you’re coding-along with this book, please use go get
to download the latest v1.N.N
release of httprouter
like so:
To demonstrate how httprouter
works, we’ll start by adding the two endpoints for creating a new movie and showing the details of a specific movie to our codebase. By the end of this chapter, our API endpoints will look like this:
Method | URL Pattern | Handler | Action |
---|---|---|---|
GET | /v1/healthcheck | healthcheckHandler | Show application information |
POST | /v1/movies | createMovieHandler | Create a new movie |
GET | /v1/movies/:id | showMovieHandler | Show the details of a specific movie |
Encapsulating the API routes
To prevent our main()
function from becoming cluttered as the API grows, let’s encapsulate all the routing rules in a new cmd/api/routes.go
file.
If you’re following along, create this new file and add the following code:
There are a couple of benefits to encapsulating our routing rules in this way. The first benefit is that it keeps our main()
function clean and ensures all our routes are defined in one single place. The other big benefit, which we demonstrated in the first Let’s Go book, is that we can now easily access the router in any test code by initializing an application
instance and calling the routes()
method on it.
The next thing that we need to do is update the main()
function to remove the http.ServeMux
declaration, and use the httprouter
instance returned by app.routes()
as our server handler instead. Like so:
Adding the new handler functions
Now that the routing rules are set up, we need to make the createMovieHandler
and showMovieHandler
methods for the new endpoints. The showMovieHandler
is particularly interesting here, because as part of this we want to extract the movie ID parameter from the URL and use it in our HTTP response.
Go ahead and create a new cmd/api/movies.go
file to hold these two new handlers:
And then add the following code:
And with that, we’re now ready to try this out!
Go ahead and restart the API application…
Then while the server is running, open a second terminal window and use curl
to make some requests to the different endpoints. If everything is set up correctly, you will see some responses which look similar to this:
Notice how, in the final example, the value of the movie id
parameter 123
has been successfully retrieved from the URL and included in the response?
You might also want to try making some requests for a particular URL using an unsupported HTTP method. For example, let’s try making a POST
request to /v1/healthcheck
:
That’s looking really good. The httprouter
package has automatically sent a 405 Method Not Allowed
response for us, including an Allow
header which lists the HTTP methods that are supported for the endpoint.
Likewise, you can make an OPTIONS
request to a specific URL and httprouter
will send back a response with an Allow
header detailing the supported HTTP methods. Like so:
Lastly, you might want to try making a request to the GET /v1/movies/:id
endpoint with a negative number or a non-numerical id
value in the URL. This should result in a 404 Not Found
response, similar to this:
Creating a helper to read ID parameters
The code to extract an id
parameter from a URL like /v1/movies/:id
is something that we’ll need repeatedly in our application, so let’s abstract the logic for this into a small reuseable helper method.
Go ahead and create a new cmd/api/helpers.go
file:
And add a new readIDParam()
method to the application
struct, like so:
With this helper method in place, the code in our showMovieHandler
can now be made a lot simpler:
Additional Information
Conflicting routes
It’s important to be aware that httprouter
doesn’t allow conflicting routes which potentially match the same request. So, for example, you cannot register a route like GET /foo/new
and another route with a parameter segment that conflicts with it — like GET /foo/:id
.
If you’re using a standard REST structure for your API endpoints — like we will be in this book — then this restriction is unlikely to cause you many problems.
In fact, it’s arguably a positive thing. Because conflicting routes aren’t allowed, there are no routing-priority rules that you need to worry about, and it reduces the risk of bugs and unintended behavior in your application.
But if you do need to support conflicting routes (for example, you might need to replicate the endpoints of an existing API exactly for backwards-compatibility), then I would recommend taking a look at chi
, Gorilla mux
or flow
instead. All of these are good routers which do permit conflicting routes.
Customizing httprouter behavior
The httprouter
package provides a few configuration options that you can use to customize the behavior of your application further, including enabling trailing slash redirects and enabling automatic URL path cleaning.
More information about the available settings can be found here.