How to write a REST API in Go with DI

sarulabs
sarulabs
Aug 2, 2018 · 9 min read

This tutorial shows how to use sarulabs/di to create a rest api in go (golang). DI is a dependency injection framework for go programs. Its role is to handle the life cycle of the services in your application. If you want to learn more about dependency injection, you can check out my previous post: what is a dependency injection container and why use one.

This post is a good complement to the sarulabs/di documentation. It assumes that you are already familiar with go and the creation of a basic http server. The code of the api is available on github: sarulabs/di-example. Most of the code is explained here, but you still will need to go to the github repository if you want to see the whole thing.

API description

The role of the api is to manage a list a car. The cars are stored in mongodb. It simplifies things for this tutorial because it is schemaless and the database is automatically created the first time it is used. It would be easy to switch to postgres or another database.

The api implements the following basic CRUD operations:

Method  URL         Role           JSON Body example
GET /cars List the cars -
POST /cars Insert a car {“brand”: “audi”, “color”: “black”}
GET /cars/{id} Get a car -
PUT /cars/{id} Update a car {“brand”: “audi”, “color”: “black”}
DELETE /cars/{id} Delete a car -

The requests and responses bodies are encoded in json. The api handles the following error codes:

  • 400 - Bad Request : the parameters of the request are not valid
  • 404 - Not Found : the car does not exist
  • 500 - Internal Error : an unexpected error occurred (eg: the database connection failed)

The api can be tested with curl or applications like postman. The github repository includes a docker-compose file that can be used to start the api without much effort.

Project structure

The project structure is as simple as this:

├─ app
│ ├─ handlers
│ ├─ middlewares
│ └─ models
│ ├─ garage
│ └─ helpers

├─ config
│ ├─ logging
│ └─ services

└─ main.go

The main.go file is the entrypoint of the application. Its role is to create a web server that can handle the api routes.

The app/handler and app/middlewares directories are, as their names say, where the handlers and the middlewares of the application are defined. They represent the controller part of an MVC application, nothing more.

app/models/garage contains the business logic. Put another way, it defines what is a car and how to manage them.

app/models/helpers consists of functions that can assist the handlers. The ReadJSONBody function can decode the body of an http request, and the JSONResponse function can write a json response. The package also include two errors type: ErrValidation and ErrNotFound. They are used to facilitate the error handling in the http handlers.

In the config/logging directory, a logger is defined as a global variable. The logger is a special object. That is because you need to have a logger as soon as possible in your application. And you also want to keep it until the application stops.

config/services is where you can find the service definitions for the dependency injection container. They describe how the services are created, and how they should be closed.

Model

The model is where you can find the business logic of the application. In our case, the model should be able to handle CRUD operations for cars. In the models/garage package, you can find the following elements:

type Car struct {
ID string `json:"id" bson:"_id"`
Brand string `json:"brand" bson:"brand"`
Color string `json:"color" bson:"color"`
}
func ValidateCar(car *Car) errortype CarManagerInterface interface {
GetAll() ([]*Car, error)
Get(id string) (*Car, error)
Create(car *Car) (*Car, error)
Update(id string, car *Car) (*Car, error)
Delete(id string) error
}
type CarRepositoryInterface interface {
FindAll() ([]*Car, error)
FindByID(id string) (*Car, error)
Insert(car *Car) error
Update(car *Car) error
Delete(id string) error
IsNotFoundErr(err error) bool
IsAlreadyExistErr(err error) bool
}

Car is the structure saved in the database. The structure is also used in the requests and responses. It represents a very simple car with only two fields, a brand and a color. The ValidateCar function can check if the brand and the color are valid. If the combination is not allowed, it returns a validation error explaining what is wrong with the given car. It is used in the creation and the update of a car. You can find the code of the function in car.go.

There are two more structures in the model. A CarManager and a CarRepository. Their interfaces CarManagerInterface and CarRepositoryInterface do not exist in the application code but are given here to illustrate what they do.

The CarManager is the structure used by the handlers to execute the CRUD operations. There is one method for each operation, or in other words, one for each http handler. The CarManager needs a CarRepository to execute the mongo queries.

Separating the database queries in a repository allows to easily list all the interactions with the database. In this situation, it is easy to replace the database. For example you can create another repository using postgres instead of mongo. It also gives you the opportunity to create a mock repository for your tests.

The CarManager adds parameter validation, logging and some error management to the repository code. Is is defined in carManager.go.

The CarRepository is just a wrapper around the mongo queries. The code is in carRepository.go and is really straightforward.

Services

The services package is where DI comes into play. You declare how to instantiate the structures from the model, as well as their dependencies. It allows the creation of a dependency injection container that can be used to retrieve the objects.

Actually, in our api, the handlers only need a CarManager to handle the requests. But it should be created with its dependencies:

CarManager
├─ Logger
└─ CarRepository
└─ Mongo connection

The service definitions can be found in config/services/services.go. Let’s start with the CarManager dependencies.

The logger is a global variable in the logging package. It could be used directly in the CarManager definition. But it still can be a good idea to create a service for the logger. It brings more homogeneity and allows the logger to be retrieved directly from the container.

The declaration is as follow:

var Services = []di.Def{
{
Name: "logger",
Scope: di.App,
Build: func(ctn di.Container) (interface{}, error) {
return logging.Logger, nil
},
},
// other services
}

The logger is in the App scope. It means it is only created once for the whole application. The Build function is called the first time to retrieve the service. After that, the same object is returned when the service is requested again.

Now we need a mongo connection. What we want first, is a pool of connections. Then each http request will use that pool to retrieve its own connection.

So we will create two services. mongo-pool in the App scope, and mongo in the Request scope:

{
Name: "mongo-pool",
Scope: di.App,
Build: func(ctn di.Container) (interface{}, error) {
// create a *mgo.Session
return mgo.DialWithTimeout(os.Getenv("MONGO_URL"), 5*time.Second)
},
Close: func(obj interface{}) error {
// close the *mgo.Session, it should be cast first
obj.(*mgo.Session).Close()
return nil
},
},
{
Name: "mongo",
Scope: di.Request,
Build: func(ctn di.Container) (interface{}, error) {
// get the pool of connections (*mgo.Session) from the container
// and retrieve a connection thanks to the Copy method
return ctn.Get("mongo-pool").(*mgo.Session).Copy(), nil
},
Close: func(obj interface{}) error {
// close the *mgo.Session, it should be cast first
obj.(*mgo.Session).Close()
return nil
},
},

The mongo service is created in each request. It uses the mongo-pool service to retrieve the connection. The mongo service can use the mongo-pool service in the Build function thanks to the Get method of the Container.

Note that it is also important to close the mongo connection in both cases. This can be done using the Close field of the definition. The Close function is called when the container is deleted. It happens at the end of each http request for the Request containers, and when the program stops for the App container.

Next is the CarRepository. It depends on the mongo service. As the mongo connection is in the Request scope, the CarRepository can not be in the App scope. It should be in the Request scope as well.

{
Name: "car-repository",
Scope: di.Request,
Build: func(ctn di.Container) (interface{}, error) {
return &garage.CarRepository{
Session: ctn.Get("mongo").(*mgo.Session),
}, nil
},
},

Finally, we can write the CarManager definition. In the same way as the CarRepository, the CarManager should be in the Request scope because of its dependencies.

{
Name: "car-manager",
Scope: di.Request,
Build: func(ctn di.Container) (interface{}, error) {
return &garage.CarManager{
Repo: ctn.Get("car-repository").(*garage.CarRepository),
Logger: ctn.Get("logger").(*zap.Logger),
}, nil
},
},

Based on these definitions, the dependency injection container can be created in the main.go file.

Handlers

The role of an http handler is simple. It must parse the incoming request, retrieve and call the suitable service and write the formatted response. All the handlers are more or less the same and can be found in cars.go. For example the GetCarHandler looks like this:

func GetCarHandler(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["carId"]
car, err := di.Get(r, "car-manager").(*garage.CarManager).Get(id) if err == nil {
helpers.JSONResponse(w, 200, car)
return
}
switch e := err.(type) {
case *helpers.ErrNotFound:
helpers.JSONResponse(w, 404, map[string]interface{}{
"error": e.Error(),
})
default:
helpers.JSONResponse(w, 500, map[string]interface{}{
"error": "Internal Error",
})
}
}

mux.Vars is just the way to retrieve the carId parameter from the url with gorilla/mux, the routing library that has been used for this project.

The purpose of the end of this code snippet is to format and write the response depending on what happened in the CarManager. The type switch allows to have different responses depending on the nature of the error.

The interesting part of the handler, is how the CarManager is retrieved from the dependency injection container. It is done with di.Get(r, "car-manager"). For this to work, the container should be included in the http.Request. You have to use a middleware to achieve that.

Middlewares

The api uses two middlewares. The first one is the PanicRecoveryMiddleware. It is used to recover from the panic that could occur in the handlers, and log the errors. It is really important because di.Get(r, "car-manager") can panic if the CarManager can not be retrieved from the container. Its code can be found in middleware.go.

The second middleware allows di.Get(r, "car-manager").(*garage.CarManager) to work by injecting the di.Container in the http.Request. The code is not in the middleware package because it is already included in the DI library with the di.HTTPMiddleware function.

func HTTPMiddleware(h http.HandlerFunc, app Container, logFunc func(msg string)) http.HandlerFunc

For each http request. A sub-container of the given app container is created. It is injected in the context.Context of the http.Request so it can be retrieved with di.Get. At the end of each request, the sub-container is deleted. The logFunc function is used to log the errors that can occur during the deletion the sub-container.

Main

The main.go file is the entrypoint of the application.

First, it should ensure that the logger will write everything before the program ends:

defer logging.Logger.Sync()

Then the dependency injection container can be created:

// create a builder
builder, err := di.NewBuilder()
if err != nil {
logging.Logger.Fatal(err.Error())
}
// add the service definitions
err = builder.Add(services.Services...)
if err != nil {
logging.Logger.Fatal(err.Error())
}
// create the app container, delete it just before the program stops
app := builder.Build()
defer app.Delete()

The last interesting thing is this part:

m := func(h http.HandlerFunc) http.HandlerFunc {
return middlewares.PanicRecoveryMiddleware(
di.HTTPMiddleware(h, app, func(msg string) {
logging.Logger.Error(msg)
}),
logging.Logger,
)
}

The m function combines the two middlewares. It can be use to apply the middlewares to the handlers.

The rest of the main file is just the configuration of the gorilla mux router and the creation of the web server.

Conclusion

Creating this small api was not hard at all. It would also be simple to extend it to handle more routes. Go is a good choice to design a rest api that has good performance, but also whose code is easy and fast to write.

Dependency injection would help make this project easier to maintain if it were to grow. Using the sarulabs/di framework allows you to separate the definition of the services from the business logic. The declarations happens in a single place, which is part of the application configuration. The services can be retrieved in the handlers by using the container stored in the request.

I hope this post has been helpful to you, and that you will consider Go and DI as an option to write your next rest api.


Originally published at www.sarulabs.com on August 2, 2018.

sarulabs

Written by

sarulabs

Software developer - Golang enthusiast

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade