You're reading a sample of this book. Get the full version here.
Let's Go Foundations › Method-based routing
Previous · Contents · Next
Chapter 2.5.

Method-based routing

Next, let’s follow HTTP good practice and restrict our application so that it only responds to requests with an appropriate HTTP method.

As we progress through our application build, our home, snippetView and snippetCreate handlers will merely retrieve information and display pages to the user, so it makes sense that these handlers should be restricted to acting on GET requests.

To restrict a route to a specific HTTP method, you can prefix the route pattern with the necessary HTTP method when declaring it, like so:

File: main.go
package main

...

func main() {
    mux := http.NewServeMux()
    // Prefix the route patterns with the required HTTP method (for now, we will
    // restrict all three routes to acting on GET requests).
    mux.HandleFunc("GET /{$}", home)
    mux.HandleFunc("GET /snippet/view/{id}", snippetView)
    mux.HandleFunc("GET /snippet/create", snippetCreate)

    log.Print("starting server on :4000")

    err := http.ListenAndServe(":4000", mux)
    log.Fatal(err)
}

It’s also worth mentioning that when you register a route pattern which uses the GET method, it will match both GET and HEAD requests. All other methods (like POST, PUT and DELETE) require an exact match.

Let’s test out this change by using curl to make some requests to our application. If you’re following along, start by making a regular GET request to http://localhost:4000/, like so:

$ curl -i localhost:4000/
HTTP/1.1 200 OK
Date: Wed, 18 Mar 2024 11:29:23 GMT
Content-Length: 21
Content-Type: text/plain; charset=utf-8

Hello from Snippetbox

The response here looks good. We can see that our route still works, and we get back a 200 OK status and a Hello from Snippetbox response body, just like before.

You can also go ahead and try making a HEAD request for the same URL. You should see that this also works correctly, returning just the HTTP response headers, and no response body.

$ curl --head localhost:4000/
HTTP/1.1 200 OK
Date: Wed, 18 Mar 2024 11:29:23 GMT
Content-Length: 21
Content-Type: text/plain; charset=utf-8

In contrast, let’s try making a POST request to http://localhost:4000/. The POST method isn’t supported for this route, so you should get an error response similar to this:

$ curl -i -d "" localhost:4000/
HTTP/1.1 405 Method Not Allowed
Allow: GET, HEAD
Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff
Date: Wed, 18 Mar 2024 11:29:23 GMT
Content-Length: 19

Method Not Allowed*

So that’s looking really good. We can see that Go’s servemux 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 request URL.

Adding a POST-only route and handler

Let’s also add a new snippetCreatePost handler to our codebase, which we’ll use later on to create and save a new snippet in a database. Because creating and saving a snippet is a non-idempotent action that will change the state of our server, we want make sure that this handler acts on POST requests only.

All in all, we’ll want our fourth handler and route to look like this:

Route pattern Handler Action
GET /{$} home Display the home page
GET /snippet/view/{id} snippetView Display a specific snippet
GET /snippet/create snippetCreate Display a form for creating a new snippet
POST /snippet/create snippetCreatePost Save a new snippet

Let’s go ahead and add the necessary code to our main.go file, like so:

File: main.go
package main

...

// Add a snippetCreatePost handler function.
func snippetCreatePost(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Save a new snippet..."))
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("GET /{$}", home)
    mux.HandleFunc("GET /snippet/view/{id}", snippetView)
    mux.HandleFunc("GET /snippet/create", snippetCreate)
    // Create the new route, which is restricted to POST requests only.
    mux.HandleFunc("POST /snippet/create", snippetCreatePost)
    
    log.Print("starting server on :4000")

    err := http.ListenAndServe(":4000", mux)
    log.Fatal(err)
}

Notice that it’s totally OK to declare two (or more) separate routes that have different HTTP methods but otherwise have the same pattern, like we are doing here with"GET /snippet/create" and "POST /snippet/create".

If you restart the application and try making some requests with the URL path /snippet/create, you should now see different responses depending on the request method that you use.

$ curl -i localhost:4000/snippet/create
HTTP/1.1 200 OK
Date: Wed, 18 Mar 2024 11:29:23 GMT
Content-Length: 50
Content-Type: text/plain; charset=utf-8

Display a form for creating a new snippet...

$ curl -i -d "" localhost:4000/snippet/create
HTTP/1.1 200 OK
Date: Wed, 18 Mar 2024 11:29:23 GMT
Content-Length: 21
Content-Type: text/plain; charset=utf-8

Save a new snippet...

$ curl -i -X DELETE localhost:4000/snippet/create
HTTP/1.1 405 Method Not Allowed
Allow: GET, HEAD, POST
Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff
Date: Wed, 18 Mar 2024 11:29:23 GMT
Content-Length: 19

Method Not Allowed

Additional information

Method precedence

The most specific pattern wins rule also applies if you have route patterns that overlap because of a HTTP method.

It’s important to be aware that a route pattern which doesn’t include a method — like "/article/{id}" — will match incoming HTTP requests with any method. In contrast, a route like "POST /article/{id}" will only match requests which have the method POST. So if you declare the overlapping routes "/article/{id}" and "POST /article/{id}" in your application, then the "POST /article/{id}" route will take precedence.

Handler naming

I’d also like to emphasize that there is no right or wrong way to name your handlers in Go.

In this project, we’ll follow a convention of postfixing the names of any handlers that deal with POST requests with the word ‘Post’. Like so:

Route pattern Handler Action
GET /snippet/create snippetCreate Display a form for creating a new snippet
POST /snippet/create snippetCreatePost Create a new snippet

But in your own work it’s not necessary to follow this pattern. For example, you could prefix handler names with the words ‘get’ and ‘post’ instead, like this:

Route pattern Handler Action
GET /snippet/create getSnippetCreate Display a form for creating a new snippet
POST /snippet/create postSnippetCreate Create a new snippet

Or even give the handlers completely different names. For example:

Route pattern Handler Action
GET /snippet/create newSnippetForm Display a form for creating a new snippet
POST /snippet/create createSnippet Create a new snippet

Basically, you have the freedom in Go to choose a naming convention for your handlers that works for you and fits with your brain.

Third-party routers

The wildcard and method-based routing functionality that we’ve been using in the past two chapters is relatively new to Go — it only became part of the standard library in Go 1.22. While this is a very welcome addition to the language and a big improvement, you might find that there are still times where the standard library routing functionality doesn’t provide everything that you need.

For example, the following things are not currently supported:

If you need these features in your application, you’ll need to use a third-party router package. The ones that I recommend are httprouter, chi, flow and gorilla/mux, and you can find a comparison of them and guidance about which one to use in this blog post.