go-api-basic

module
v0.9.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 13, 2019 License: MIT

README

go-API-basic

A RESTful API template (built with Go) - work in progress...

The goal of this repo/API is to make an example/template of a relational database-backed REST HTTP API that has characteristics needed to ensure success in a high volume environment. I'm gearing this towards beginners, as I struggled with a lot of this over the past couple of years and would like to help others getting started. I have another repo which is really an extension of this one, but has more components (like request/response logging via the httplog module). I'm working on the documentation for that, but wanted to have this repo for simplicity sake.

Thanks / Attribution

I should say that most of the ideas I'm presenting here are not my own - I learned them from reading a number of books and blogs from extremely talented individuals. Here is a list (in no particular order) of influences:

API Walkthrough

The following is an in-depth walkthrough of this repo as well as the module dependencies that are called within. This walkthrough has a stupid amount of detail. This is a demo API, so the "business" intent of it is to support basic CRUD (Create, Read, Update, Delete) operations for a movie database (as of this writing, it's only Create, but the goal is to have examples for everything).

Errors

Before even getting into the full walkthrough, I wanted to review the errors module and the approach taken for error handling. The errors module is basically a carve out of the error handling used in the upspin library with some tweaks and additions I made for my own needs. Rob Pike has a fantastic post about errors and the Upspin implementation. I've taken that and added my own twist.

My general idea for error handling throughout this API and dependent modules is to always raise an error using the errors.E function as seen in this simple error handle below. errors.E is neat - you can pass in any one of a number of approved types and the function helps form the error. In all error cases, I pass the errors.Op as the errors.E function helps build a pseudo stack trace for the error as it goes up through the code. Here's a snippet showing a typical, simple example of using the errors.E function.

func NewServer(name env.Name, lvl zerolog.Level) (*Server, error) {
    const op errors.Op = "server/NewServer"

    // call constructor for Env struct from env module
    env, err := env.NewEnv(name, lvl)
    if err != nil {
        return nil, errors.E(op, err)
    }

The following snippet shows a more robust validation example. In it, you'll notice that if you need to define your own error quickly, you can just use a string and that becomes the error string as well.

func (m *Movie) validate() error {
    const op errors.Op = "movie/Movie.validate"

    switch {
    case m.Title == "":
        return errors.E(op, errors.Validation, errors.Parameter("Title"), errors.MissingField("Title"))
    case m.Year < 1878:
        return errors.E(op, errors.Validation, errors.Parameter("Year"), "The first film was in 1878, Year must be >= 1878")

In the above snippet, the errors.MissingField function used to validate missing input on fields comes from this Mat Ryer post and is pretty handy.

// MissingField is an error type that can be used when
// validating input fields that do not have a value, but should
type MissingField string

func (e MissingField) Error() string {
    return string(e) + " is required"
}

As stated before, as errors go up the stack from whatever depth of code they're in, Upspin captures the operation and adds that to the error string as a pseudo stack trace that is super helpful for debugging. However, I don't want this type of internal stack information exposed to end users in the response - I only want the error message. As such, just prior to shipping the response, I log the error (to capture the stack info) and call a custom function I built called errors.RE (Response Error). This function effectively strips the stack information and just sends the original error message along with whatever http status code you select as well as whatever errors.Kind, Code or Parameter you choose to set. The RE function returns an error of type errors.HTTPErr. An example of error handling at the highest level (from the POST handler) is below:

// Call the create method of the Movie object to validate and insert the data
err = movie.Create(ctx, s.Logger, tx)
if err != nil {
    // log error
    s.Logger.Error().Err(err).Msg("")
    // Type assertion is used - all errors should be an *errors.Error type
    // Use Kind, Param, Code and Error from lower level errors to populate RE (Response Error)
    if e, ok := err.(*errors.Error); ok {
        err := errors.RE(http.StatusBadRequest, e.Kind, e.Param, e.Code, err)
        errors.HTTPError(w, err)
        return
    }

    // if falls through type assertion, then serve an unanticipated error
    err := errors.RE(http.StatusInternalServerError, errors.Unanticipated)
    errors.HTTPError(w, err)
    return
}

The final statement above before returning the errors is a call to the errors.HTTPError function. This function determines if an error is of type errors.HTTPErr and if so, forms the error json - the response body will look something like this:

{
    "error": {
        "kind": "input_validation_error",
        "param": "Year",
        "message": "The first film was in 1878, Year must be >= 1878"
    }
}
Main API Module

The Main API/server layer module is the starting point and as such has the main package/function within the cmd directory. In it, I'm checking for both log level and environment command line flags and running them through 2 simple functions (logLevel and envName) to get the correct string value given the flag input. I'm using zerolog throughout my modules as the logger.

func main() {

    // loglvl flag allows for setting logging level, e.g. to run the server
    // with level set to debug, it'd be: ./server loglvl=debug
    loglvlFlag := flag.String("loglvl", "error", "sets log level")

    // env flag allows for setting environment, e.g. Production, QA, etc.
    // example: env=dev, env=qa, env=stg, env=prod
    envFlag := flag.String("env", "dev", "sets log level")

    flag.Parse()

    // get appropriate zerolog.Level based on flag
    lvl := logLevel(loglvlFlag)
    log.Log().Msgf("Logging Level set to %s", lvl)

    // get appropriate env.Name based on flag
    eName := envName(envFlag)
    log.Log().Msgf("Environment set to %s", eName)

Next in the main, I'm calling the NewServer function from the server package to construct a new server using the environment name and logging level.

    // call constructor for Server struct
    server, err := server.NewServer(eName, lvl)
    if err != nil {
        log.Fatal().Err(err).Msg("")
    }

The server's multiplex router is Gorilla, which is registered as the handler for http.Handle.

    // handle all requests with the Gorilla router
    http.Handle("/", server.Router)

Finally, http.ListenAndServe is run to truly start the server and listen for incoming requests.

    // ListenAndServe on port 8080, not specifying a particular IP address
    // for this particular implementation
    if err := http.ListenAndServe(":8080", nil); err != nil {
        log.Fatal().Err(err).Msg("")
    }

Let's dig into the server.NewServer constructor function from above. In the server package, there is also a Server struct, which is composed of a pointer to the Env struct from the env module, as below:

// Server struct contains the environment (env.Env) and additional methods
// for running our HTTP server
type Server struct {
    *env.Env
}

The Server struct uses type embedding, which allows it to take on all the properties of the Env struct from the env module (things like database setup, logger, multiplexer, etc.), but also allows for extending the struct with API-specific methods for routing logic and handlers.

The Env struct in the env module has the following structure:

// Env struct stores common environment related items
type Env struct {
    // Environment Name (e.g. Production, QA, etc.)
    Name Name
    // multiplex router
    Router *mux.Router
    // Datastore struct containing AppDB (PostgreSQL),
    // LogDb (PostgreSQL) and CacheDB (Redis)
    DS *datastore.Datastore
    // Logger
    Logger zerolog.Logger
}

Within the server.NewServer constructor function, I'm calling the NewEnv function of the env module. The env.NewEnv constructor function does all the database setup, starts the logger as well as initializes the gorilla multiplexer. Fore more information on what's happening inside the env.NewEnv function, reference the env module Readme

// NewServer is a constructor for the Server struct
// Sets up the struct and registers routes
func NewServer(name env.Name, lvl zerolog.Level) (*Server, error) {
    const op errors.Op = "server/NewServer"

    // call constructor for Env struct from env module
    env, err := env.NewEnv(name, lvl)
    if err != nil {
        return nil, errors.E(op, err)
    }

After getting env.Env back, I embed it in the Server struct

    // Use type embedding to make env.Env struct part of Server struct
    server := &Server{env}

Finally, I call the server.routes method and return the server.

    // routes registers handlers to the Server router
    err = server.routes()
    if err != nil {
        return nil, errors.E(op, err)
    }

    return server, nil

Inside the server.routes method, first I pull out the app database from the server struct to pass into my servertoken handler.

// routes registers handlers to the router
func (s *Server) routes() error {
    const op errors.Op = "server/Server.routes"

    // Get App Database for token authentication
    appdb, err := s.DS.DB(datastore.AppDB)
    if err != nil {
        return errors.E(op, err)
    }

Next, the URL path and handlers are register to the router embedded in the server.

    s.Router.Handle("/v1/movie",
        alice.New(
            s.handleStdResponseHeader,
            servertoken.Handler(s.Logger, appdb)).
            ThenFunc(s.handlePost())).
        Methods("POST").
        Headers("Content-Type", "application/json")

The Methods("POST"). means this route will only take POST request, and for REST this means we're looking at our Create method of the (CRUD) we talked about above. Other methods (Read(GET), Update(PUT), and Delete(DELETE)) will be documented later. The Headers("Content-Type", "application/json") means that this route requires that this request header be present.

To go through the v1/movie Handle registration item by item - my own fork as a module of Justinas Stankevičius' alice library is being used to make middleware chaining easier. Hopefully the original alice library will enable modules and I'll go back, but until then I'll keep my own fork as it has properly setup modules files.

Next, the first middleware in the chain above s.handleStdResponseHeader simply adds standard response headers. As of now, it's just the Content-Type:application/json header, but it's an easy place to other headers one may deem standard.

// handleStdResponseHeader middleware is used to add standard HTTP response headers
func (s *Server) handleStdResponseHeader(h http.Handler) http.Handler {
    return http.HandlerFunc(
        func(w http.ResponseWriter, r *http.Request) {
            w.Header().Add("Content-Type", "application/json")
            h.ServeHTTP(w, r) // call original
        })
}

Next, the second middleware in the chain above (servertoken.Handler) validates that the caller of the API is authorized. Details for what's happening in this servertoken module can be found here.

The final handler, s.handlePost, which hangs off the Server struct mentioned above, is meant to handle POST requests to the route registered for the API ("/v1/movie").

Inside the handlePost method, a private request and response struct are defined. I like this technique as it gives me complete control over what is coming and going from the API.

// handlePost handles POST requests for the /movie endpoint
// and creates a movie in the database
func (s *Server) handlePost() http.HandlerFunc {
    return func(w http.ResponseWriter, req *http.Request) {

        // request is the expected service request fields
        type request struct {
            Title    string `json:"Title"`
            Year     int    `json:"Year"`
            Rated    string `json:"Rated"`
            Released string `json:"ReleaseDate"`
            RunTime  int    `json:"RunTime"`
            Director string `json:"Director"`
            Writer   string `json:"Writer"`
        }

        // response is the expected service response fields
        type response struct {
            request
            CreateTimestamp string `json:"CreateTimestamp"`
        }

The request is Decoded into an instance of the request struct.

        // Declare rqst as an instance of request
        // Decode JSON HTTP request body into a Decoder type
        // and unmarshal that into rqst
        rqst := new(request)
        err := json.NewDecoder(req.Body).Decode(&rqst)
        defer req.Body.Close()
        if err != nil {
            err = errors.RE(http.StatusBadRequest, errors.InvalidRequest, err)
            errors.HTTPError(w, err)
            return
        }

The request is mapped to the business struct (movie.Movie) from the movie module.

// Movie holds details of a movie
type Movie struct {
    Title    string
    Year     int
    Rated    string
    Released time.Time
    RunTime  int
    Director string
    Writer   string
    dbaudit.Audit
}

As part of the mapping, quick input validations around date formatting are done (see time.Parse below)

        // dateFormat is the expected date format for any date fields
        // in the request
        const dateFormat string = "Jan 02 2006"

        // declare a new instance of movie.Movie
        movie := new(movie.Movie)
        movie.Title = rqst.Title
        movie.Year = rqst.Year
        movie.Rated = rqst.Rated
        t, err := time.Parse(dateFormat, rqst.Released)
        if err != nil {
            err = errors.RE(http.StatusBadRequest,
                errors.Validation,
                errors.Code("invalid_date_format"),
                errors.Parameter("ReleaseDate"),
                err)
            errors.HTTPError(w, err)
            return
        }
        movie.Released = t
        movie.RunTime = rqst.RunTime
        movie.Director = rqst.Director
        movie.Writer = rqst.Writer

The context is pulled from the incoming request and a database transaction is started using the AppDB from the Server struct

        // retrieve the context from the http.Request
        ctx := req.Context()

        // get a new DB Tx from the PostgreSQL datastore within the server struct
        tx, err := s.DS.BeginTx(ctx, nil, datastore.AppDB)
        if err != nil {
            err = errors.RE(http.StatusInternalServerError, errors.Database)
            errors.HTTPError(w, err)
            return
        }

The Create method of the movie.Movie struct is called using the context and database transaction from above as well as the Logger from the server. The error handling is important here, but it is discussed at length in the errors section. For more information on what's happening inside the movie module check the Readme here. In summary though, the movie module has the "business logic" for the API - it is doing deeper input validations, exercising any business rules and creating/reading/updating/deleting the data in the database (as well as handling commit or rollback).

        // Call the create method of the Movie object to validate and insert the data
        err = movie.Create(ctx, s.Logger, tx)
        if err != nil {
            // log error
            s.Logger.Error().Err(err).Msg("")
            // Type assertion is used - all errors should be an *errors.Error type
            // Use Kind, Param, Code and Error from lower level errors to populate RE (Response Error)
            if e, ok := err.(*errors.Error); ok {
                err := errors.RE(http.StatusBadRequest, e.Kind, e.Param, e.Code, err)
                errors.HTTPError(w, err)
                return
            }

            // if falls through type assertion, then serve an unanticipated error
            err := errors.RE(http.StatusInternalServerError, errors.Unanticipated)
            errors.HTTPError(w, err)
            return
        }

If we got this far, the db transaction has been created/committed - we can consider this transaction successful and return a response. An instance of the response struct is initialized and populated with data from the movie.Movie struct and the response is encoded and sent back to the caller!

        // create a new response struct and set Audit and other
        // relevant elements
        resp := new(response)
        resp.Title = movie.Title
        resp.Year = movie.Year
        resp.Rated = movie.Rated
        resp.Released = movie.Released.Format(dateFormat)
        resp.RunTime = movie.RunTime
        resp.Director = movie.Director
        resp.Writer = movie.Writer
        resp.CreateTimestamp = movie.CreateTimestamp.Format(time.RFC3339)

        // Encode response struct to JSON for the response body
        json.NewEncoder(w).Encode(*resp)
        if err != nil {
            err = errors.RE(http.StatusInternalServerError, errors.Internal)
            errors.HTTPError(w, err)
            return
        }

Questions/Concerns? Want more detail? Feel free to open an issue and label it appropriately. Thanks!

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL