Building an API Framework in Go


head

In the article Building a Form Validation and Processing API in Go, we defined how a microservice API can be created to handle validating and processing form data. In the further work section we noted:

“We can generalize our manager service and have it handle the boiler plate work…” The goal being that we want to simplify the code in zefram around some conventions. As we continue to express our opinion in code, we’re making decisions that lead toward a framework and set of conventions we’ll use in other projects.

We’ve removed the manager package and abstracted that into a new package, ls-governor, which provides a comprehensive API:

1// API contains a fibre web service and our management service.
2type API struct {
3	WebService *fibre.WebService
4	ManagerService *ManagerService
5}

We’ve also abstracted out our manager service, which references a new package for handling datastore configurations and connections, ls-superbase, which favors gorm for our object relational mapper.

1// ManagerService contains the configuration settings required to manage the api.
2type ManagerService struct {
3	Config   *toml.Tree
4	DBConfig map[string]*superbase.DBConfig
5}

Now we’ve far less complexity to manage in Zefram. Our root command for setting up and running Zefram is now:

cmd/root.go:

 1rootCmd = &cobra.Command{
 2    Use:   "zefram -c [config.toml]",
 3    Short: "run zefram",
 4    Long:  `run zefram with config.toml as a daemon`,
 5    Run: func(cmd *cobra.Command, args []string) {
 6        gms := &governor.ManagerService{}
 7        if config == "" {
 8            config = "config.toml"
 9        }
10
11        // setup manager and create api
12        gms.InitManager(config)
13        gms.InitDatastore("zefram")
14        gapi := gms.CreateAPI("zefram")
15
16        // bridge logic
17        model.Migrate(gapi, "zefram")
18        api.SetupRoutes(gapi)
19
20        // now daemonize the api
21        gms.Daemonize(gapi)
22    },
23}

We’ve created a new package, ls-governor, which handles our manager service, and integrating it with our api and models.
This means zefram’s new api package consists of two files. A routes definition and related handlers.

To allow governor to bridge our route logic with handlers that have access to the full governor API, we’ll wrap them in a function ls-fibre’s Mux Router expects.

pkg/api/routes.go:

 1// routes handles setting up routes for our API
 2package api
 3
 4import (
 5	"net/http"
 6
 7	"github.com/lakesite/ls-governor"
 8)
 9
10// SetupRoutes defines and associates routes to handlers.
11// Use a wrapper convention to pass a governor API to each handler.
12func SetupRoutes(gapi *governor.API) {
13	gapi.WebService.Router.HandleFunc(
14		"/zefram/api/v1/contact/", 
15		func(w http.ResponseWriter, r *http.Request) {
16			ContactHandler(w, r, gapi)
17		},
18	).Methods("POST")
19}

Now we can write handlers with full access to governor’s API, which includes a datastore and app configuration settings.

pkg/api/handlers.go:

 1// handlers contains the handlers to manage API endpoints
 2package api
 3
 4import (
 5	"fmt"
 6	"net/http"
 7
 8	valid "github.com/asaskevich/govalidator"
 9	"github.com/gorilla/schema"
10	"github.com/lakesite/ls-mail"
11	"github.com/lakesite/ls-governor"
12
13	"github.com/lakesite/zefram/pkg/models"
14)
15
16// ContactHandler handles POST data for a Contact.
17// The handler is wrapped to provide convenience access to a governor.API
18func ContactHandler(w http.ResponseWriter, r *http.Request, gapi *governor.API) {
19	// parse the form
20	err := r.ParseForm()
21	if err != nil {
22		gapi.WebService.JsonStatusResponse(w, "Error parsing form data.", http.StatusBadRequest)
23		return
24	}
25
26	// create a new contact
27	c := new(model.Contact)
28
29	// using a new decoder, decode the form and bind it to the contact
30	decoder := schema.NewDecoder()
31	decoder.Decode(c, r.Form)
32
33	// validate the structure:
34	_, err = valid.ValidateStruct(c)
35	if err != nil {
36		gapi.WebService.JsonStatusResponse(w, fmt.Sprintf("Error: %s", err.Error()), http.StatusBadRequest)
37		return
38	}
39
40	// insert the contact structure
41	if dbc := gapi.ManagerService.DBConfig["zefram"].Connection.Create(c); dbc.Error != nil {
42		gapi.WebService.JsonStatusResponse(w, fmt.Sprintf("Error: %s", dbc.Error.Error()), http.StatusInternalServerError)
43		return
44	}
45
46	// send an email
47	mailfrom, _ := gapi.ManagerService.GetAppProperty("zefram", "mailfrom")
48	mailto, _ := gapi.ManagerService.GetAppProperty("zefram", "mailto")
49	subject := "Contact from: " + c.Email
50	body := "First Name: " + c.FirstName + "\nLast Name: " + c.LastName + "\nMessage: \n\n" + c.Message
51	err = mail.LocalhostSendMail(mailfrom, mailto, subject, body)
52
53	if err != nil {
54		gapi.WebService.JsonStatusResponse(w, fmt.Sprintf("Error: %s", err.Error()), http.StatusInternalServerError)
55		return
56	}
57
58	// Return StatusOK with Contact made:
59	gapi.WebService.JsonStatusResponse(w, "Contact made", http.StatusOK)
60}

Further, we can push the business logic in the handler out into our model when our project grows in complexity. Our model convention now has two files, contact.go and migrations.go. We haven’t changed anything in our contact.go file, which has our contact model.

Our migrations.go file uses the governor API and it’s datastore connection to handle a gorm migration:

pkg/models/migrations.go:

 1package model
 2
 3import (
 4	"errors"
 5	"fmt"
 6
 7	"github.com/lakesite/ls-governor"
 8)
 9
10// Migrate takes a governor API and app name and migrates models, returns error
11func Migrate(gapi *governor.API, app string) error {
12	if gapi == nil {
13		return errors.New("Migrate: Governor API is not initialized.")
14	}
15
16	if app == "" {
17		return errors.New("Migrate: App name cannot be empty.")
18	}
19
20	dbc := gapi.ManagerService.DBConfig[app]
21	
22	if dbc == nil {
23		return fmt.Errorf("Migrate: Database configuration for '%s' does not exist.", app)
24	}
25
26	if dbc.Connection == nil {
27		return fmt.Errorf("Migrate: Database connection for '%s' does not exist.", app)
28	}
29
30	dbc.Connection.AutoMigrate(&Contact{})
31	return nil
32}

We call Migrate in cmd/root.go, and for now, use gorm’s auto migration feature.

Using governor, our new project layout for zefram looks like this:

    cmd/root.go
    docs
    pkg/api
        handlers.go
        routes.go
    pkg/models
        contact.go
        migrations.go
    config.toml
    LICENSE.md
    main.go
    Makefile
    README.md

Logic simplified.

comments powered by Disqus