Go API Development: Part 1 - Routes & Handlers
Overview
This is the first article in a series of articles on developing a web API using Golang. In this series I will walk you through developing a simple REST API in Go using the Service Pattern with complete examples of Dependency Injection, Mocking, and a simple Redis Caching layer. We’ll be using sqlite as our database backend.
If you wish to skip the articles and prefer to just read the code, you can find the go-contacts
project on GitHub.
In this article I am going to give an overview of the project layout and also demonstrate how to setup the API routes and HTTP handlers for the API.
Contacts API
We will be developing a REST API to manage Contacts. The Contacts API specification is defined as follows:
Method | Path | Description |
---|---|---|
GET | /users/:userid/contacts |
List a user’s contacts |
POST | /users/:userid/contacts |
Create a contact |
Project Layout
Before we jump right in, I want to give you an overview of how I like to organize my projects. This should help you understand some of the rationale behind why I put code where I do, and hopefully it will become apparent why certain code goes into certain files/packages.
I like to organize Go web API projects using this folder structure:
|- controllers
|- datastores
|- interfaces
|- models
|- mocks
|- services
main.go
router.go
This folder structure is inspired from Ichsan Rahardianto’s service-pattern-go
page.
The folder structure is created to accomodate seperation of concern principles, where every struct should have single responsibility to achieve decoupled system.
Every folder is a namespace of their own, and every file / struct under the same folder should only use the same namepace as their root folder.
main.go
- The main entry point of the Contacts API. The main method triggers the ChiRouter singleton and initializes (binds) the routes and handlers by calling InitRouter.
package main
...
func main() {
http.ListenAndServe(":8080", NewChiRouter().InitRouter())
}
router.go
- Binds the controller’s handlers to the appropriate route to handle the HTTP request. In this example we’re using the Chi router, which is a “lightweight, idiomatic and composable router for building Go HTTP services.” I like using Chi because it’s 100% compliant with the standard Go net/http library, which would allow me to swap it out for another net/http compliant library if I so choose.
package main
...
type ChiRouter interface {
InitRouter() *chi.Mux
}
type router struct{}
func (router *router) InitRouter() *chi.Mux {
// Create the SQLite DB Handler
// Covered in the next article in this series.
var sqliteHandler interfaces.DBHandler
// Inject all implementations of the interfaces.
controller := controllers.ContactsController{
&services.ContactsService{
DataAccessor: &datastores.ContactsDatastore{
sqliteHandler,
},
},
}
// Define and bind the API routes for the Contacts API
r := chi.NewRouter()
r.Get("/users/{userid}/contacts", controller.ListUserContacts)
r.Post("/users/{userid}/contacts", controller.CreateContact)
return r
}
var (
m *router
routerOnce sync.Once
)
// NewChiRouter defines a Singleton, ensuring only a single ChiRouter is created
func NewChiRouter() ChiRouter {
if m == nil {
routerOnce.Do(func() {
m = &router{}
})
}
return m
}
Controllers
Controllers are responsible for handling the HTTP requests coming into the router. The controller layer should not implement service logic and data access. The service and data access layers should be done seperately..
Controllers must implement services through their interface. Service interface implementations should NOT be done in the controller so as to maintain decoupled logic. The implementation will be injected during compile time.
controllers/ContactsController.go
package controllers
...
type ContactsController struct {
ContactManager interfaces.ContactsManager
}
func (controller *ContactsController) ListUserContacts(w http.ResponseWriter, req *http.Request) {
userID := chi.URLParam(req, "userid")
contacts, err := controller.ContactManager.ListUserContacts(userID)
if err != nil {
// Handle error
}
json.NewEncoder(w).Encode(contacts)
return
}
func (controller *ContactsController) CreateContact(w http.ResponseWriter, req *http.Request) {
userID := chi.URLParam(req, "userid")
contact := models.Contact{}
// See go-chi/render package
if err := render.Bind(req, &contact); err != nil {
// Handle error
}
createdContact, err := controller.ContactManager.CreateContact(userID, contact)
if err != nil {
// Handle error
}
json.NewEncoder(w).Encode(createdContact)
}
Interfaces
interfaces/ContactsManager.go
- Defines the interface to manage Contacts. The ContactsService
struct will implement this interface.
package interfaces
...
type ContactsManager interface {
ListUserContacts(userID string) ([]models.Contact, error)
CreateContact(userID string, contact models.Contact) (models.Contact, error)
}
interfaces/ContactsDataAccessor.go
- Defines the data access layer interface to manage the persistence of Contact information. The ContactsService
struct embeds this interface to interact with the data access layer.
package interfaces
...
type ContactsDataAccessor interface {
ListUserContacts(userID string) ([]models.Contact, error)
CreateUserContact(userID string, contact models.Contact) (models.Contact, error)
}
interfaces/DBHandler.go
- Defines the interfaces to interact with a SQL database backend. We will implement this interface in the next article in the series.
package interfaces
...
type DBHandler interface {
Query(query string, args ...interface{}) (DBRow, error)
Execute(query string, args ...interface{}) (sql.Result, error)
}
type DBRow interface {
Scan(dest ...interface{}) error
Next() bool
}
Models
The models folder houses the structs under the models
namespace. Each model defines a struct that reflects the data object(s) serialized and deserialized to/from the database layer.
models/users.go
- Defines the model for a User in the API.
package models
type User struct {
ID string `json:"id"`
Contacts []Contact `json:"contacts"`
}
models/contacts.go
- Defines the model for a Contact in the API.
package models
...
type Contact struct {
ID string `json:"id"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
Phone string `json:"phone"`
Email string `json:"email,omitempty"`
}
// Bind on Contact will run after the unmarshalling is complete, its
// a good time to focus some post-processing after a decoding.
func (c *Contact) Bind(r *http.Request) error {
return nil
}
Services
The services folder houses structs under the services
namespace. This folder is where the business logic should live. Structs living in this folder should handle the fetching of data from the data access layer and run the business logic needed to satisfy what the controller expects the Contacts API to return.
services/ContactsService.go
- Defines the service which implements the ContactManager
interface. In this example there isn’t any business logic beyond interacting with the data access layer. However, in your application this may not be the case..
package services
...
type ContactsService struct {
DataAccessor interfaces.ContactsDataAccessor
}
func (service *ContactsService) ListUserContacts(userID string) ([]models.Contact, error) {
return service.DataAccessor.ListUserContacts(userID)
}
func (service *ContactsService) CreateContact(userID string, contact models.Contact) (models.Contact, error) {
return service.DataAccessor.CreateUserContact(userID, contact)
}
Datastores
The datastores folder houses structs under datastores
namespace. This folder is where the implementation of the data access layer should live. All queries and data operation to/from the database should happen here, and the implementation should be agnostic of what backend database engine is used and how the queries are done.
datastores/ContactsDatastore.go
- Defines the implementation of the ContactsDataAccessor
interface.
package datastores
...
type ContactsDatastore struct {
interfaces.DbHandler
}
func (ds *ContactsDatastore) ListUserContacts(userID string) ([]models.Contact, error) {
... // Covered in the next article in this series.
}
func (ds *ContactsDatastore) CreateUserContact(userID string, contact models.Contact) (models.Contact, error) {
... // Covered in the next article in this series.
}
Summary
At this point you should have some Go code that defines a pretty good skeleton of the Contacts API. We have created a main application entry point that binds various API routes to HTTP handlers defined by a controller, and we have wired up the controller with the interfaces it needs to do it’s job. We have also implemented a service struct that implements the ContactsManager
interface, so we’re one step closer to having a working application. In “Go API Development: Part 2” we will implement the ContactsDataAccessor
interface so that we can start to actually persist the Contacts that we manage.