HTTP Methods are Verbs

Handling HTTP methods

  • HTTP methods are verbs, and are used to indicate the type of action that the client is requesting
  • In RESTful APIs they are are used to indicate the action to be performed on a resource
  • So a typical task of the code is to analyse the method and decide what to do
  • We’ve seen how to use a switch statement to check the method and act accordingly
switch r.Method {
case http.MethodGet:
    // Handle GET requests in a private method.
    h.handleGet(w, r)
case http.MethodPost:
    // Handle POST requests in a private method.
    h.handlePost(w, r)
default:
    // Handle other methods or return error.
    ...
}

HTTPX

But why repeat this for every handler?

  • Let’s create our HTTP helper package, named e.g. pkg/httpx
// Package httpx contains helper functions for the daily work with HTTP.
package httpx
  • Use the power of interfaces to distribute HTTP methods
  • Create our method wrapper in pkg/httpx/methods.go
  • It defines individual interfaces for each HTTP method
  • The wrapper implements the http.Handler interface and contains the handler with the business logic
  • Its ServeHTTP distributes the requests to the handler methods based on a type switch
package httpx

import (
    "net/http"
)

// GetHandler has to be implemented by a handler for GET requests
// dispatched through the MethodHandler.
type GetHandler interface {
    ServeHTTPGet(w http.ResponseWriter, r *http.Request)
}

// PostHandler has to be implemented by a handler for POST requests
// dispatched through the MethodHandler.
type PostHandler interface {
    ServeHTTPPost(w http.ResponseWriter, r *http.Request)
}

// PutHandler has to be implemented by a handler for PUT requests
// dispatched through the MethodHandler.
type PutHandler interface {
    ServeHTTPPut(w http.ResponseWriter, r *http.Request)
}

// DeleteHandler has to be implemented by a handler for DELETE requests
// dispatched through the MethodHandler.
type DeleteHandler interface {
    ServeHTTPDelete(w http.ResponseWriter, r *http.Request)
}

// ...

// MethodHandler wraps a http.Handler implementing also individual httpx handler
// interfaces. It distributes the requests to the handler methods based on a type
// switch In case of no matching method a http.ErrMethodNotAllowed is returned.
type MethodHandler struct {
    handler http.Handler
}

func NewMethodHandler(h http.Handler) *MethodHandler {
    return &MethodHandler{
        handler: h,
    }
}

// ServeHTTP implements the http.Handler interface.
func (h *MethodHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    switch r.Method {
    case http.MethodGet:
        if hh, ok := h.handler.(GetHandler); ok {
            hh.ServeHTTPGet(w, r)
            return
        }
    case http.MethodPost:
        if hh, ok := h.handler.(PostHandler); ok {
            hh.ServeHTTPPost(w, r)
            return
        }
     case http.MethodPut:
        if hh, ok := h.handler.(PutHandler); ok {
            hh.ServeHTTPPut(w, r)
            return
        }
    case http.MethodDelete:
        if hh, ok := h.handler.(DeleteHandler); ok {
            hh.ServeHTTPDelete(w, r)
            return
        }
    // ...
    }
    // Fall back to default for no matching handler method or any
    // other HTTP method.
    h.handler.ServeHTTP(w, r)
}

Example

The new cache server package

  • Re-implementing the cache handler using the method wrapper
package cache

import (
    "log"
    "net/http"
    "sync"
)

// Handler provides a simple in-memory cache server. Cache is done
// via a map of string to []byte. The sync.RWMutex is used to ensure that
// the cache is thread-safe.
type Handler struct {
    mu    sync.RWMutex
    cache map[string][]byte
}

// Handler provides a simple in-memory cache server. Cache is done
// via a map of string to []byte. The sync.RWMutex is used to ensure that
// the cache is thread-safe.
type Handler struct {
    mu    sync.RWMutex
    cache map[string][]byte
}

// NewHandler creates the cache server. It's simply needed to create
// the map of string to []byte.
func NewHandler() *Handler {
    return &Handler{
        cache: make(map[string][]byte),
    }
}

// ServeHTTPGet handles GET requests. If the path can be found in the cache,
// its data is returned. Otherwise, the response is set to 404. Only
// read lock is needed.
func (h *Handler) ServeHTTPGet(w http.ResponseWriter, r *http.Request) {
    h.mu.RLock()
    defer h.mu.RUnlock()

    // Check if path is known.
    data, ok := h.cache[r.URL.Path]
    if !ok {
        w.WriteHeader(http.StatusNotFound)
        return
    }
    w.WriteHeader(http.StatusOK)
    w.Write(data)
}

// ...

Using the cache server with the wrapper

package main

import (
    "log"
    "net/http"

    "./pkg/cache"
    "./pkg/httpx"
)

// main runs the cache server.
func main() {
    h := cache..NewHandler()
    mh := httpx.NewMethodHandler(h) // Use the method handler as wrapper.
    err := http.ListenAndServe(":8080", mh)

    if err != nil {
        log.Fatal(err)
    }
}