[Part III] How To Build a RESTful JSON API in Go - API

Concluding our build of a RESTful JSON API with a Postgres database, we will explore the code that binds the backend queries to the HTTP methods. Instead of using the builtin networking tools for HTTP, I decided to use echo.

Disclaimer

A true RESTful service uses HTTP methods coupled with Unified Resource Identifiers to traverse an application. In this example, the input data from the client is also JSON.

Server

As we discussed in the previous part of the tutorial, by adding JSON tags we can embed the database configuration. That way we can load both configs from the same file.

type (  
    // Config for the server and database
    Config struct {
        Server struct {
            Bind    string `json:"bind"`
            Cert    string `json:"cert"`
            Key     string `json:"key"`
            LogPath string `json:"log,omitempty"`
        } `json:"server"`
        Database database.Config `json:"database"`
    }
)

With our configuration file modeled, all we need to do is read it into memory and Unmarshal() the data into the structure. From there we can call the method receivers on both Database{} and Server{} for setup. NewServer() creates an echo instance, binds middleware, and groups routes for the User API. It also calls database.Config.NewStore() which creates and passes back a connection to the database.

// NewConfig loads the configurations into a structure
// to be used as a global operator at runtime
func NewConfig(path string) (conf *Config) {  
    data, _ := ioutil.ReadFile(path)
    if err := json.Unmarshal(data, &conf); err != nil {
        return nil
    }
    return conf
}

// NewServer returns a configured api server instance
func (c *Config) NewServer() (*echo.Echo, error) {  
    e := echo.New()
    if logFile, err := os.OpenFile(c.Server.LogPath, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0660); err != nil {
        e.Use(middleware.Logger())
    } else {
        e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{Output: logFile}))
    }
    e.Use(middleware.Recover())
    api := e.Group("/api/v1")
    if store, err := c.Database.NewStore(); err == nil {
        data := &Data{store}
        handlers := &Handlers{User: data}
        api.POST("/user", handlers.User.CreateUser)
        api.GET("/user", handlers.User.ReadUser)
        api.PUT("/user", handlers.User.UpdateUser)
        api.DELETE("/user", handlers.User.DeleteUser)
    } else {
        return nil, err
    }
    return e, nil
}
User API

In NewServer() there is a variable called handlers which is where all of the HTTP handlers for the echo routes are defined. Handlers{} are essentially a definition of the CRUD methods and some defined type. They can be used because *database.Store{} is embedded in Data{} and the interface UserCRUD{} is implemented as pointer receiver methods for Data{}. Therefore by initializing Handlers.User with *Data{}, the pointer receiver methods are available to Handlers.User so they can be passed as a handler for the route.

type (  
    // Handlers are the HTTP handlers to be used by the API router
    Handlers struct {
        User UserCRUD
    }
)

All that is left to do from here is implement each CRUD method of the UserCRUD interface. The example below shows how I implemented CreateUser(). Since echo.Context is passed to this pointer method I use an anonymous structure to bind to the POST data. That way I can control which data the user actually gets to define. If binding fails the server sends an 400 error and returns. If it passes then a database.User is created from the request. Since this is a pointer method with the database embedded in it, all that is left is to pass the database.Query that represents the newly defined database.User. Depending on the transaction response, a success or error message is sent back.

func (d *Data) CreateUser(c echo.Context) error {  
    bind := struct {
        First string `json:"first"`
        Last  string `json:"last"`
        Role  string `json:"role"`
    }{}
    if err := c.Bind(&bind); err != nil {
        return c.JSON(http.StatusBadRequest, map[string]interface{}{
            "code":    1010,
            "message": "invalid user or json format in request body",
        })
    }
    user := &database.User{First: bind.First, Last: bind.Last, Role: bind.Role}
    if err := d.EWT(user.Create()); err != nil {
        return c.JSON(http.StatusInternalServerError, map[string]interface{}{
            "code":    1011,
            "message": "user could not be created",
        })
    }
    return c.JSON(http.StatusOK, map[string]interface{}{
        "code":    1000,
        "message": "user was successfully created",
    })
}

There are multiple ways to create a RESTful API in golang even though it is an opinionated language. I came from a Ruby on Rails background and I found this structure to be similar.

Pasquale D'Agostino

Read more posts by this author.