This project is a fully production-ready solution designed to implement best practices for building performant and secure backend REST API services. It provides a robust architectural framework to ensure consistency and maintain high code quality. The architecture emphasizes feature separation, facilitating easier unit and integration testing.
- Go
- Gin
- jwt
- mongodriver
- go-redis
- Validator
- Viper
- Crypto
Highlights
- API key support
- Token based Authentication
- Role based Authorization
- Unit Tests
- Integration Tests
- Modular codebase
The goal is to make each API independent from one another and only share services among them. This will make code reusable and reduce conflicts while working in a team.
The APIs will have separate directory based on the endpoint. Example blog
and blogs
will have seperate directory whereas blog
, blog/author
, and blog/editor
will share common resources and will live inside same directory.
cmd/main → startup/server → module, mongo, redis, router → api/[feature]/middlewares → api/[feature]/controller -> api/[feature]/service, authentication, authorization → handlers → sender
Sample API
├── dto
│ └── create_sample.go
├── model
│ └── sample.go
├── controller.go
└── service.go
- Each feature API lives under
api
directory - The request and response body is sent in the form of a DTO (Data Transfer Object) inside
dto
directory - The database collection model lives inside
model
directory - Controller is responsible for defining endpoints and corresponding handlers
- Service is the main logic component and handles data. Controller interact with a service to process a request. A service can also interact with other services.
- api: APIs code
- arch: It provide framework and base implementation for creating the architecture
- cmd: main function to start the program
- common: code to be used in all the apis
- config: load environment variables
- keys: stores server pem files for token
- startup: creates server and initializes database, redis, and router
- tests: holds the integration tests
- utils: contains utility functions
Helper/Optional Directories
- .extra: mongo script for initialization inside docker, other web assets and documents
- .github: CI for tests
- .tools: api code, RSA key generator, and .env copier
- .vscode: editor config and debug launch settings
vscode is the recommended editor - dark theme
1. Get the repo
git clone https://github.com/unusualcodeorg/goserve.git
2. Generate RSA Keys
go run .tools/rsa/keygen.go
3. Create .env files
go run .tools/copy/envs.go
4. Run Docker Compose
- Install Docker and Docker Compose. Find Instructions Here.
docker-compose up --build
- You will be able to access the api from http://localhost:8080
5. Run Tests
docker exec -t goserver go test -v ./...
If having any issue
- Make sure 8080 port is not occupied else change SERVER_PORT in .env file.
- Make sure 27017 port is not occupied else change DB_PORT in .env file.
- Make sure 6379 port is not occupied else change REDIS_PORT in .env file.
go mod tidy
Keep the docker container for mongo
and redis
running and stop the goserve
docker container
Change the following hosts in the .env and .test.env
- DB_HOST=localhost
- REDIS_HOST=localhost
Best way to run this project is to use the vscode Run and Debug
button. Scripts are available for debugging and template generation on vscode.
go run cmd/main.go
New api creation can be done using command. go run .tools/apigen.go [feature_name]
. This will create all the required skeleton files inside the directory api/[feature_name]
go run .tools/apigen.go sample
How to Architect Good Go Backend REST API Services
You can use goservegen CLI to generate starter project for this architecture.
Check out the repo github.com/unusualcodeorg/goservegen for more information.
Information about the framework
api/sample/model/sample.go
package model
import (
"context"
"time"
"github.com/go-playground/validator/v10"
"github.com/unusualcodeorg/goserve/arch/mongo"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
mongod "go.mongodb.org/mongo-driver/mongo"
)
const CollectionName = "samples"
type Sample struct {
ID primitive.ObjectID `bson:"_id,omitempty" validate:"-"`
Field string `bson:"field" validate:"required"`
Status bool `bson:"status" validate:"required"`
CreatedAt time.Time `bson:"createdAt" validate:"required"`
UpdatedAt time.Time `bson:"updatedAt" validate:"required"`
}
func NewSample(field string) (*Sample, error) {
time := time.Now()
doc := Sample{
Field: field,
Status: true,
CreatedAt: time,
UpdatedAt: time,
}
if err := doc.Validate(); err != nil {
return nil, err
}
return &doc, nil
}
func (doc *Sample) GetValue() *Sample {
return doc
}
func (doc *Sample) Validate() error {
validate := validator.New()
return validate.Struct(doc)
}
func (*Sample) EnsureIndexes(db mongo.Database) {
indexes := []mongod.IndexModel{
{
Keys: bson.D{
{Key: "_id", Value: 1},
{Key: "status", Value: 1},
},
},
}
mongo.NewQueryBuilder[Sample](db, CollectionName).Query(context.Background()).CreateIndexes(indexes)
}
arch/mongo/database
type Document[T any] interface {
EnsureIndexes(Database)
GetValue() *T
Validate() error
}
api/sample/dto/create_sample.go
package dto
import (
"fmt"
"time"
"github.com/go-playground/validator/v10"
"go.mongodb.org/mongo-driver/bson/primitive"
)
type InfoSample struct {
ID primitive.ObjectID `json:"_id" binding:"required"`
Field string `json:"field" binding:"required"`
CreatedAt time.Time `json:"createdAt" binding:"required"`
}
func EmptyInfoSample() *InfoSample {
return &InfoSample{}
}
func (d *InfoSample) GetValue() *InfoSample {
return d
}
func (d *InfoSample) ValidateErrors(errs validator.ValidationErrors) ([]string, error) {
var msgs []string
for _, err := range errs {
switch err.Tag() {
case "required":
msgs = append(msgs, fmt.Sprintf("%s is required", err.Field()))
case "min":
msgs = append(msgs, fmt.Sprintf("%s must be min %s", err.Field(), err.Param()))
case "max":
msgs = append(msgs, fmt.Sprintf("%s must be max %s", err.Field(), err.Param()))
default:
msgs = append(msgs, fmt.Sprintf("%s is invalid", err.Field()))
}
}
return msgs, nil
}
arch/network/interfaces.go
type Dto[T any] interface {
GetValue() *T
ValidateErrors(errs validator.ValidationErrors) ([]string, error)
}
api/sample/service.go
package sample
import (
"github.com/unusualcodeorg/goserve/api/sample/dto"
"github.com/unusualcodeorg/goserve/api/sample/model"
"github.com/unusualcodeorg/goserve/arch/mongo"
"github.com/unusualcodeorg/goserve/arch/network"
"github.com/unusualcodeorg/goserve/arch/redis"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
)
type Service interface {
FindSample(id primitive.ObjectID) (*model.Sample, error)
}
type service struct {
network.BaseService
sampleQueryBuilder mongo.QueryBuilder[model.Sample]
infoSampleCache redis.Cache[dto.InfoSample]
}
func NewService(db mongo.Database, store redis.Store) Service {
return &service{
BaseService: network.NewBaseService(),
sampleQueryBuilder: mongo.NewQueryBuilder[model.Sample](db, model.CollectionName),
infoSampleCache: redis.NewCache[dto.InfoSample](store),
}
}
func (s *service) FindSample(id primitive.ObjectID) (*model.Sample, error) {
filter := bson.M{"_id": id}
msg, err := s.sampleQueryBuilder.SingleQuery().FindOne(filter, nil)
if err != nil {
return nil, err
}
return msg, nil
}
arch/network/interfaces.go
type BaseService interface {
Context() context.Context
}
- Database Query:
mongo.QueryBuilder[model.Sample]
provide the methods to make common mongo queries for the modelmodel.Sample
- Redis Cache:
redis.Cache[dto.InfoSample]
provide the methods to make common redis queries for the DTOdto.InfoSample
api/sample/controller.go
package sample
import (
"github.com/gin-gonic/gin"
"github.com/unusualcodeorg/goserve/api/sample/dto"
"github.com/unusualcodeorg/goserve/common"
coredto "github.com/unusualcodeorg/goserve/arch/dto"
"github.com/unusualcodeorg/goserve/arch/network"
"github.com/unusualcodeorg/goserve/utils"
)
type controller struct {
network.BaseController
common.ContextPayload
service Service
}
func NewController(
authMFunc network.AuthenticationProvider,
authorizeMFunc network.AuthorizationProvider,
service Service,
) network.Controller {
return &controller{
BaseController: network.NewBaseController("/sample", authMFunc, authorizeMFunc),
ContextPayload: common.NewContextPayload(),
service: service,
}
}
func (c *controller) MountRoutes(group *gin.RouterGroup) {
group.GET("/id/:id", c.getSampleHandler)
}
func (c *controller) getSampleHandler(ctx *gin.Context) {
mongoId, err := network.ReqParams(ctx, coredto.EmptyMongoId())
if err != nil {
c.Send(ctx).BadRequestError(err.Error(), err)
return
}
sample, err := c.service.FindSample(mongoId.ID)
if err != nil {
c.Send(ctx).NotFoundError("sample not found", err)
return
}
data, err := utils.MapTo[dto.InfoSample](sample)
if err != nil {
c.Send(ctx).InternalServerError("something went wrong", err)
return
}
c.Send(ctx).SuccessDataResponse("success", data)
}
arch/network/interfaces.go
type Controller interface {
BaseController
MountRoutes(group *gin.RouterGroup)
}
type BaseController interface {
ResponseSender
Path() string
Authentication() gin.HandlerFunc
Authorization(role string) gin.HandlerFunc
}
type ResponseSender interface {
Debug() bool
Send(ctx *gin.Context) SendResponse
}
type SendResponse interface {
SuccessMsgResponse(message string)
SuccessDataResponse(message string, data any)
BadRequestError(message string, err error)
ForbiddenError(message string, err error)
UnauthorizedError(message string, err error)
NotFoundError(message string, err error)
InternalServerError(message string, err error)
MixedError(err error)
}
startup/module.go
import (
...
"github.com/unusualcodeorg/goserve/api/sample"
)
...
func (m *module) Controllers() []network.Controller {
return []network.Controller{
...
sample.NewController(m.AuthenticationProvider(), m.AuthorizationProvider(), sample.NewService(m.DB, m.Store)),
}
}
startup/indexes.go
import (
...
sample "github.com/unusualcodeorg/goserve/api/sample/model"
)
func EnsureDbIndexes(db mongo.Database) {
go mongo.Document[sample.Sample](&sample.Sample{}).EnsureIndexes(db)
...
}
goserve
also provides micro
package to build REST API microservices. Find the microservices version of this blog service project at github.com/unusualcodeorg/gomicro
Article - How to Create Microservices — A Practical Guide Using Go
- Support it by clicking the ⭐ button on the upper right of this page. ✌️
Subscribe to the YouTube channel UnusualCode
for understanding the concepts used in this project:
Please feel free to fork it and open a PR.