Building a RESTful API service in Go without repetitive boilerplate
There are a lot of materials about how to write services, where at first you need to choose some framework to use, then comes wiring of handlers, configs, logs, storage, etc, not to mention deploying that service somewhere. We’ve been writing services for quite some time and more often than not you’d just want to skip all this tedious process of gluing stuff together and just write some useful code.
That’s why we created a tool, called Mify — it’s an open-source infrastructure boilerplate generator, which would help you build a service, taking the best practices used to date. So in this tutorial, we’ll show how to create a simple service using Mify with a classic example — a to-do app.
Prerequisites
- Install Go 1.18+, Docker
- Get Mify from our GitHub: https://github.com/mify-io/mify
- Install Postman or curl to test endpoints
Before starting this tutorial, here’s the link to the complete example: https://github.com/mify-io/todo-app-example
Creating Project
First we need to initialize the project workspace, this is would be a place where backend service will be created, but you can create more services and frontends as well. Run these commands:
$ mify init todo-app
$ cd todo-app
After getting into new workspace, run:
$ mify add service todo-backend
Now, this will create a Go template for your to-do backend. Here’s a simplified tree of the workspace with all generated files:
.
├── go-services
│ ├── cmd
│ │ ├── dev-runner
│ │ │ └── main.go
│ │ └── todo-backend
│ │ ├── Dockerfile
│ │ └── main.go
│ ├── go.mod
│ ├── go.sum
│ └── internal
│ ├── pkg
│ │ └── generated
│ │ ├── configs
│ │ │ └── ...
│ │ ├── consul
│ │ │ └── ...
│ │ ├── logs
│ │ │ └── ...
│ │ └── metrics
│ │ └── ...
│ └── todo-backend
│ ├── app
│ │ ├── request_extra.go
│ │ ├── router
│ │ │ └── router.go
│ │ └── service_extra.go
│ └── generated
│ ├── api
| | └── ...
│ ├── app
│ │ └── ...
│ ├── apputil
│ │ └── ...
│ └── core
│ └── ...
├── schemas
│ └── todo-backend
│ ├── api
│ │ └── api.yaml
│ └── service.mify.yaml
└── workspace.mify.yaml
Mify loosely follows the layout from https://github.com/golang-standards/project-layout, which is, despite the name of the repository, is not standard, but pretty common for Go services. In internal/pkg/generated
there are common libraries for configs, logs, and metrics which can be reused for multiple services. Your service go-to directory is in internal/todo-backend
.
At this point this service is pretty bare, so we need to add API to it.
Defining API
Mify allows you to define API with OpenAPI schema — this way you’ll get all handlers ready without writing a lot of boilerplate. Even more, Mify generates structured logs and metrics as well, which would’ve taken weeks to write manually.
You can find the OpenAPI schema for todo-backend in schemas/todo-backend/api/api.yaml
file. Schemas directory in the root of the workspace is a place where all service configs related to Mify are stored.
Let’s create a simple CRUD API for your todo backend:
POST /todos
for adding new todo notes.PUT,GET,DELETE /todos/{id}
for updating, retrieving and deleting them.
Here’s how your OpenAPI schema would look for this API:
Replace the previous schema with this one and run mify generate
. You can run it each time you update the schema and it will regenerate all the changed stuff.
Building and Testing
Now that we’ve added handlers, there is something to test, so let’s do it. In a terminal inside the Mify workspace go to go-services
directory and run the service:
$ cd go-services
$ go mod tidy
$ go run ./cmd/todo-backend
You should see startup logs like that:
You can see the service port in starting api server
log message, copy it to Postman and try calling some API handler:
You can see that handler returned nothing, which is expected because as the error suggests, it’s not implemented yet.
Adding Models and Mock Storage
Let’s start adding some logic to this service. In this tutorial, we’ll follow some simple DDD-like (Domain-driven design) structure.
First, we need to create a model for the todo note, we’ll put it in the domain
package
in go-services/internal/todo-backend/domain/todo.go
:
This is also a good place to define the interface for storage, which is useful for decoupling persistence logic from the application. In this tutorial, we’ll use mock storage, in memory, but Mify also supports Postgres, which we can add later in a follow-up article. Let’s put storage in go-services/internal/todo-backend/storage/todo_mem.go
:
Before jumping to handlers we also should implement an application layer to decouple storage logic, but in this example, this layer will be just a proxy to it.
internal/todo-backend/application/todo.go
:
That’s all for logic, we just need to add it to handlers.
Implementing Handlers
Mify generates handler stubs in go-services/internal/todo-backend/handlers/<path/to/api>/service.go
. In our case these will be in
go-services/internal/todo-backend/handlers/todos/service.go
for POST method, and
go-services/internal/todo-backend/handlers/todos/id/service.go
for others.
Here’s an example of a stub of the POST method:
Now, let’s implement all handlers.
go-services/internal/todo-backend/handlers/todos/service.go
Don’t forget to update imports:
import (
"net/http"
"strconv"
"example.com/namespace/todo-app/go-services/internal/todo-backend/domain"
"example.com/namespace/todo-app/go-services/internal/todo-backend/generated/api"
"example.com/namespace/todo-app/go-services/internal/todo-backend/generated/apputil"
"example.com/namespace/todo-app/go-services/internal/todo-backend/generated/core"
"example.com/namespace/todo-app/go-services/internal/todo-backend/handlers"
)
go-services/internal/todo-backend/handlers/todos/id/service.go
And here’s imports for them as well:
import (
"errors"
"fmt"
"net/http"
"strconv"
"example.com/namespace/todo-app/go-services/internal/todo-backend/domain"
"example.com/namespace/todo-app/go-services/internal/todo-backend/generated/api"
"example.com/namespace/todo-app/go-services/internal/todo-backend/generated/apputil"
"example.com/namespace/todo-app/go-services/internal/todo-backend/generated/core"
"example.com/namespace/todo-app/go-services/internal/todo-backend/handlers"
"example.com/namespace/todo-app/go-services/internal/todo-backend/storage"
)
The handler’s logic is pretty simple, we’re just converting generated OpenAPI models to our application one and back, and to avoid duplication in code, here’s a helper for creating TodoNode response, which is used in these implementations:
go-services/internal/todo-backend/handlers/common.go
:
Testing Again
Finally the service is fully implemented and we can test it!
First we can add a new todo note with POST request:
Check if it is added with GET request:
Update it with PUT request:
Delete it:
And run GET once again to check if it was deleted:
What’s Next
There is a still a lot of stuff not covered in this article like:
- Persistent storage, like Postgres,
- Configuration,
- Authentication middleware,
- Deployment to the cloud.
Most of this stuff is covered in our docs, so check them out: https://mify.io/docs, but stay tuned for the next articles.