goserve is a robust Go backend architecture. It offers a performant and scalable framework, emphasizing feature separation, clean code, and testability. Ideal for REST API development, goserve simplifies unit and integration testing, ensuring high-quality, production-ready applications with ease.
APACHE-2.0 License
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.
Highlights
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
api
directorydto
directorymodel
directoryHelper/Optional Directories
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
docker-compose up --build
5. Run Tests
docker exec -t goserver go test -v ./...
If having any issue
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
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
}
mongo.QueryBuilder[model.Sample]
provide the methods to make common mongo queries for the model model.Sample
redis.Cache[dto.InfoSample]
provide the methods to make common redis queries for the DTO dto.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
Subscribe to the YouTube channel UnusualCode
for understanding the concepts used in this project:
Please feel free to fork it and open a PR.