Janishar Ali
•
15 Jan 2026

Let me start by asking a question, what is the best way to write software?
You will definitely say this is a vague question. It will depend on what you are building, how much you can look into the future, and most importantly how much time and money you can spend in developing the best possible solution for the given problem.
This iterative approach has resulted in some rule of thumb. My article and the goserve backend framework which is the topic of this article is about those learnings which will help you to reduce the iterations while developing REST API services from scratch.
Now, you may argue that the best solution is to work on microservices and prepare for the scale that eventually needs to be addressed. For that I will say “Yes”. I will also quote “You don’t need a bomb to kill an ant”. For the remaining part of this article, I will discuss the solution that is typically needed when you start building your product to get PMF (product market fit).
Let’s begin with those requirements in mind. So, you have to write a backend API service, now which framework to choose, which pattern to follow, and most of all which language to adopt. Or simply why Go?
I have worked on many languages and frameworks, and until recently I have been writing backend services in Javascript/Typescript and I see myself writing many more such services in the future with them. I have also written extensively in Java and Kotlin. So, I can surely tell you, that no one language is superior to another, but each one has its beauty. But we might choose one over the other based on many considerations, maybe job availability or performance. Go is unique in this respect, it is relatively new and adopts simplicity over a bunch of features. It does not have similar generics capabilities like Java or freedom like Javascript. But it is a language that you will appreciate when you experience the simplicity of writing programs and freedom from the ocean of libraries which other languages heavily rely on.
Go is compiled but feels like an interpreted language and its fast in both compilation and execution. So, here is the reason to give Go a try.
Now, let’s come back to the topic, what are the main requirements for a common REST API service?
Few more things to consider
“For all these points a framework is very useful.”
A Quick tour of the goserve backend framework — GitHub Repo Link
Sample Project Structure:
.
├── .extra
│ └── setup
│ └── init-mongo.js
├── api
│ └── sample
│ ├── dto
│ │ └── create_sample.go
│ ├── model
│ │ └── sample.go
│ ├── controller.go
│ └── service.go
├── cmd
│ └── main.go
├── config
│ └── env.go
├── keys
│ ├── private.pem
│ └── public.pem
├── startup
│ ├── indexes.go
│ ├── module.go
│ ├── server.go
│ └── testserver.go
├── utils
│ └── convertor.go
├── .env
├── .test.env
├── .gitignore
├── .dockerignore
├── Dockerfile
├── docker-compose.yml
├── go.mod
└── go.sum
Let’s take bottom to top approach. I am going to discuss the goserve example for blog service implementation.
Note: The blog service example has more apis implemented as compared to the sample project structure given above.
You will start the program from cmd/main.go. The main function just calls the Server function in the package startup
package main
import "github.com/afteracademy/goserve/startup"
func main() {
startup.Server()
}
What will the Server function do?
It will assemble all the things required to enable the server to do its job.
Who are the main characters in this story?
Who are the side characters of the story?
Who are the villains in the story?
Let’s see the request-response flow for the public, private, and protected APIs.

Now let’s come back to the Server
package startup
import (
"context"
"time"
"github.com/gin-gonic/gin"
"github.com/afteracademy/goserve/arch/mongo"
"github.com/afteracademy/goserve/arch/network"
"github.com/afteracademy/goserve/arch/redis"
"github.com/afteracademy/goserve/config"
)
type Shutdown = func()
func Server() {
env := config.NewEnv(".env", true)
router, _, shutdown := create(env)
defer shutdown()
router.Start(env.ServerHost, env.ServerPort)
}
func create(env *config.Env) (network.Router, Module, Shutdown) {
context := context.Background()
dbConfig := mongo.DbConfig{
User: env.DBUser,
Pwd: env.DBUserPwd,
Host: env.DBHost,
Port: env.DBPort,
Name: env.DBName,
MinPoolSize: env.DBMinPoolSize,
MaxPoolSize: env.DBMaxPoolSize,
Timeout: time.Duration(env.DBQueryTimeout) * time.Second,
}
db := mongo.NewDatabase(context, dbConfig)
db.Connect()
if env.GoMode != gin.TestMode {
EnsureDbIndexes(db)
}
redisConfig := redis.Config{
Host: env.RedisHost,
Port: env.RedisPort,
Pwd: env.RedisPwd,
DB: env.RedisDB,
}
store := redis.NewStore(context, &redisConfig)
store.Connect()
module := NewModule(context, env, db, store)
router := network.NewRouter(env.GoMode)
router.RegisterValidationParsers(network.CustomTagNameFunc())
router.LoadRootMiddlewares(module.RootMiddlewares())
router.LoadControllers(module.Controllers())
shutdown := func() {
db.Disconnect()
store.Disconnect()
}
return router, module, shutdown
}
The first thing the server needs is an environment variable holder i.e config/env.go. It simply reads .env file and populates Env object.
package config
import (
"log"
"github.com/spf13/viper"
)
type Env struct {
// server
GoMode string `mapstructure:"GO_MODE"`
ServerHost string `mapstructure:"SERVER_HOST"`
ServerPort uint16 `mapstructure:"SERVER_PORT"`
// database
DBHost string `mapstructure:"DB_HOST"`
DBName string `mapstructure:"DB_NAME"`
DBPort uint16 `mapstructure:"DB_PORT"`
DBUser string `mapstructure:"DB_USER"`
DBUserPwd string `mapstructure:"DB_USER_PWD"`
DBMinPoolSize uint16 `mapstructure:"DB_MIN_POOL_SIZE"`
DBMaxPoolSize uint16 `mapstructure:"DB_MAX_POOL_SIZE"`
DBQueryTimeout uint16 `mapstructure:"DB_QUERY_TIMEOUT_SEC"`
// redis
RedisHost string `mapstructure:"REDIS_HOST"`
RedisPort uint16 `mapstructure:"REDIS_PORT"`
RedisPwd string `mapstructure:"REDIS_PASSWORD"`
RedisDB int `mapstructure:"REDIS_DB"`
// keys
RSAPrivateKeyPath string `mapstructure:"RSA_PRIVATE_KEY_PATH"`
RSAPublicKeyPath string `mapstructure:"RSA_PUBLIC_KEY_PATH"`
// Token
AccessTokenValiditySec uint64 `mapstructure:"ACCESS_TOKEN_VALIDITY_SEC"`
RefreshTokenValiditySec uint64 `mapstructure:"REFRESH_TOKEN_VALIDITY_SEC"`
TokenIssuer string `mapstructure:"TOKEN_ISSUER"`
TokenAudience string `mapstructure:"TOKEN_AUDIENCE"`
}
func NewEnv(filename string, override bool) *Env {
env := Env{}
viper.SetConfigFile(filename)
if override {
viper.AutomaticEnv()
}
err := viper.ReadInConfig()
if err != nil {
log.Fatal("Error reading environment file", err)
}
err = viper.Unmarshal(&env)
if err != nil {
log.Fatal("Error loading environment file", err)
}
return &env
}
Next, it connects to a Mongo database, and then to a Redis database. goserveprovides a simple abstraction for these operations.
It then creates the instances of all the characters mentioned above. In this list Module and Router have the main role.
The Router is part of the goserve framework and it creates functions over the Ginto simplify the process of creating and mounting handlers.
Let’s check the module. It implements the framework’s Module interface, and create services, controllers, and middlewares. It acts as the dependency manager to create and distribute object instances. Here RootMiddlewares is attached to all the requests. But the interesting part is AuthenticationProvider and AuthorizationProvider. They basically are passes to each controller so that they can decide which endpoints need them.
type Module[T any] interface {
GetInstance() *T
RootMiddlewares() []RootMiddleware
Controllers() []Controller
AuthenticationProvider() AuthenticationProvider
AuthorizationProvider() AuthorizationProvider
}
The main idea here is to make controllers independent in deciding over the access control.
package startup
import (
"context"
"github.com/afteracademy/goserve/api/auth"
authMW "github.com/afteracademy/goserve/api/auth/middleware"
"github.com/afteracademy/goserve/api/blog"
"github.com/afteracademy/goserve/api/blog/author"
"github.com/afteracademy/goserve/api/blog/editor"
"github.com/afteracademy/goserve/api/blogs"
"github.com/afteracademy/goserve/api/contact"
"github.com/afteracademy/goserve/api/user"
coreMW "github.com/afteracademy/goserve/arch/middleware"
"github.com/afteracademy/goserve/arch/mongo"
"github.com/afteracademy/goserve/arch/network"
"github.com/afteracademy/goserve/arch/redis"
"github.com/afteracademy/goserve/config"
)
type Module network.Module[module]
type module struct {
Context context.Context
Env *config.Env
DB mongo.Database
Store redis.Store
UserService user.Service
AuthService auth.Service
BlogService blog.Service
}
func (m *module) GetInstance() *module {
return m
}
func (m *module) Controllers() []network.Controller {
return []network.Controller{
auth.NewController(m.AuthenticationProvider(), m.AuthorizationProvider(), m.AuthService),
user.NewController(m.AuthenticationProvider(), m.AuthorizationProvider(), m.UserService),
blog.NewController(m.AuthenticationProvider(), m.AuthorizationProvider(), m.BlogService),
author.NewController(m.AuthenticationProvider(), m.AuthorizationProvider(), author.NewService(m.DB, m.BlogService)),
editor.NewController(m.AuthenticationProvider(), m.AuthorizationProvider(), editor.NewService(m.DB, m.UserService)),
blogs.NewController(m.AuthenticationProvider(), m.AuthorizationProvider(), blogs.NewService(m.DB, m.Store)),
contact.NewController(m.AuthenticationProvider(), m.AuthorizationProvider(), contact.NewService(m.DB)),
}
}
func (m *module) RootMiddlewares() []network.RootMiddleware {
return []network.RootMiddleware{
coreMW.NewErrorCatcher(), // NOTE: this should be the first handler to be mounted
authMW.NewKeyProtection(m.AuthService),
coreMW.NewNotFound(),
}
}
func (m *module) AuthenticationProvider() network.AuthenticationProvider {
return authMW.NewAuthenticationProvider(m.AuthService, m.UserService)
}
func (m *module) AuthorizationProvider() network.AuthorizationProvider {
return authMW.NewAuthorizationProvider()
}
func NewModule(context context.Context, env *config.Env, db mongo.Database, store redis.Store) Module {
userService := user.NewService(db)
authService := auth.NewService(db, env, userService)
blogService := blog.NewService(db, store, userService)
return &module{
Context: context,
Env: env,
DB: db,
Store: store,
UserService: userService,
AuthService: authService,
BlogService: blogService,
}
}
The Router interface is provided by the goserve framework, and its default implementation is also provided by the framework in network.NewRouter
type Router interface {
GetEngine() *gin.Engine
RegisterValidationParsers(tagNameFunc validator.TagNameFunc)
LoadControllers(controllers []Controller)
LoadRootMiddlewares(middlewares []RootMiddleware)
Start(ip string, port uint16)
}
Let’s now focus on the apis.
The main goal is to make each api independent and isolated as much as possible. I call it feature encapsulation. Basically, one developer working on an api can work without much worry about other developer working on some other api.
For most part, these are one-time setups and you can use the starter template generator for goserve Framework namely goservegen (GitHub Repo Link)
Now let’s check a model. It implements the interface Document provided by the goserve framework mongo.
type Document[T any] interface {
EnsureIndexes(Database)
GetValue() *T
Validate() error
}
Message Model implementation
package model
import (
"time"
"github.com/go-playground/validator/v10"
"github.com/afteracademy/goserve/arch/mongo"
"go.mongodb.org/mongo-driver/bson/primitive"
)
const CollectionName = "messages"
type Message struct {
ID primitive.ObjectID `bson:"_id,omitempty" validate:"-"`
Type string `bson:"type" validate:"required"`
Msg string `bson:"msg" validate:"required"`
Status bool `bson:"status" validate:"required"`
CreatedAt time.Time `bson:"createdAt" validate:"required"`
UpdatedAt time.Time `bson:"updatedAt" validate:"required"`
}
func NewMessage(msgType string, msgTxt string) (*Message, error) {
time := time.Now()
m := Message{
Type: msgType,
Msg: msgTxt,
Status: true,
CreatedAt: time,
UpdatedAt: time,
}
if err := m.Validate(); err != nil {
return nil, err
}
return &m, nil
}
func (message *Message) GetValue() *Message {
return message
}
func (message *Message) Validate() error {
validate := validator.New()
return validate.Struct(message)
}
func (*Message) EnsureIndexes(db mongo.Database) {
}
Let’s come to the Dto Interface provided by the goserve framework
type Dto[T any] interface {
GetValue() *T
ValidateErrors(errs validator.ValidationErrors) ([]string, error)
}
Dto implementation at api/contact/dto/create_message.go
Here ValidateErrors is needed to send personalised errors, and it is called when the controller parses the body into a Dto.
package dto
import (
"fmt"
"github.com/go-playground/validator/v10"
)
type CreateMessage struct {
Type string `json:"type" binding:"required,min=2,max=50"`
Msg string `json:"msg" binding:"required,min=0,max=2000"`
}
func EmptyCreateMessage() *CreateMessage {
return &CreateMessage{}
}
func (d *CreateMessage) GetValue() *CreateMessage {
return d
}
func (d *CreateMessage) 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
}
The Controller interface provided by the goserve framework is quite involved, and I will give only a brief introduction here. In later articles, I will talk in detail about it. The goserve defines the following interfaces for the Controller functionality.
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)
}
type ResponseSender interface {
Debug() bool
Send(ctx *gin.Context) SendResponse
}
type BaseController interface {
ResponseSender
Path() string
Authentication() gin.HandlerFunc
Authorization(role string) gin.HandlerFunc
}
type Controller interface {
BaseController
MountRoutes(group *gin.RouterGroup)
}
Let’s see our controller implementation at api/contact/message/controller.go
package contact
import (
"github.com/gin-gonic/gin"
"github.com/afteracademy/goserve/api/contact/dto"
"github.com/afteracademy/goserve/arch/network"
"github.com/afteracademy/goserve/utils"
)
type controller struct {
network.BaseController
service Service
}
func NewController(
authProvider network.AuthenticationProvider,
authorizeProvider network.AuthorizationProvider,
service Service,
) network.Controller {
return &controller{
BaseController: network.NewBaseController("/contact", authProvider, authorizeProvider),
service: service,
}
}
func (c *controller) MountRoutes(group *gin.RouterGroup) {
group.POST("/", c.createMessageHandler)
}
func (c *controller) createMessageHandler(ctx *gin.Context) {
body, err := network.ReqBody(ctx, &dto.CreateMessage{})
if err != nil {
c.Send(ctx).BadRequestError(err.Error(), err)
return
}
msg, err := c.service.SaveMessage(body)
if err != nil {
c.Send(ctx).InternalServerError("something went wrong", err)
return
}
data, err := utils.MapTo[dto.InfoMessage](msg)
if err != nil {
c.Send(ctx).InternalServerError("something went wrong", err)
return
}
c.Send(ctx).SuccessDataResponse("message received successfully!", data)
}
How to process the request?
Now let’s see the api/contact/service.go
It defines an interface and implements it. This is very useful while testing using mock.
package contact
import (
"github.com/afteracademy/goserve/api/contact/dto"
"github.com/afteracademy/goserve/api/contact/model"
coredto "github.com/afteracademy/goserve/arch/dto"
"github.com/afteracademy/goserve/arch/mongo"
"github.com/afteracademy/goserve/arch/network"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
)
type Service interface {
SaveMessage(d *dto.CreateMessage) (*model.Message, error)
FindMessage(id primitive.ObjectID) (*model.Message, error)
FindPaginatedMessage(p *coredto.Pagination) ([]*model.Message, error)
}
type service struct {
network.BaseService
messageQueryBuilder mongo.QueryBuilder[model.Message]
}
func NewService(db mongo.Database) Service {
return &service{
BaseService: network.NewBaseService(),
messageQueryBuilder: mongo.NewQueryBuilder[model.Message](db, model.CollectionName),
}
}
func (s *service) SaveMessage(d *dto.CreateMessage) (*model.Message, error) {
msg, err := model.NewMessage(d.Type, d.Msg)
if err != nil {
return nil, err
}
result, err := s.messageQueryBuilder.SingleQuery().InsertAndRetrieveOne(msg)
if err != nil {
return nil, err
}
return result, nil
}
func (s *service) FindMessage(id primitive.ObjectID) (*model.Message, error) {
filter := bson.M{"_id": id}
msg, err := s.messageQueryBuilder.SingleQuery().FindOne(filter, nil)
if err != nil {
return nil, err
}
return msg, nil
}
func (s *service) FindPaginatedMessage(p *coredto.Pagination) ([]*model.Message, error) {
filter := bson.M{"status": true}
msgs, err := s.messageQueryBuilder.SingleQuery().FindPaginated(filter, p.Page, p.Limit, nil)
if err != nil {
return nil, err
}
return msgs, nil
}
Here messageQueryBuilder provides functions to make mongo queries.
For fyi lets see the Query builder interface provided by the framework
type QueryBuilder[T any] interface {
GetCollection() *mongo.Collection
SingleQuery() Query[T]
Query(context context.Context) Query[T]
}
type Query[T any] interface {
Close()
CreateIndexes(indexes []mongo.IndexModel) error
FindOne(filter bson.M, opts *options.FindOneOptions) (*T, error)
FindAll(filter bson.M, opts *options.FindOptions) ([]*T, error)
FindPaginated(filter bson.M, page int64, limit int64, opts *options.FindOptions) ([]*T, error)
InsertOne(doc *T) (*primitive.ObjectID, error)
InsertAndRetrieveOne(doc *T) (*T, error)
InsertMany(doc []*T) ([]primitive.ObjectID, error)
InsertAndRetrieveMany(doc []*T) ([]*T, error)
UpdateOne(filter bson.M, update bson.M) (*mongo.UpdateResult, error)
UpdateMany(filter bson.M, update bson.M) (*mongo.UpdateResult, error)
DeleteOne(filter bson.M) (*mongo.DeleteResult, error)
}
Note: A Service takes in Dto in most cases and sends out Dto as well.
Phew!! It was quite a long writing session. I will write individually on these topics in the next articles. So, do follow me to receive them once I publish.
Oh, I forgot, Authentication and Authorization. You can find them in the repo — Link
Let’s see how to use them in api/blog/author/controller.go
func (c *controller) MountRoutes(group *gin.RouterGroup) {
group.Use(c.Authentication(), c.Authorization(string(userModel.RoleCodeAuthor)))
group.POST("/", c.postBlogHandler)
group.PUT("/", c.updateBlogHandler)
group.GET("/id/:id", c.getBlogHandler)
group.DELETE("/id/:id", c.deleteBlogHandler)
group.PUT("/submit/id/:id", c.submitBlogHandler)
group.PUT("/withdraw/id/:id", c.withdrawBlogHandler)
group.GET("/drafts", c.getDraftsBlogsHandler)
group.GET("/submitted", c.getSubmittedBlogsHandler)
group.GET("/published", c.getPublishedBlogsHandler)
}
You call the Authentication and Authorization provider function and receive the handler function.
Now, you can explore the repo in detail and I am sure you will find it a good time-spending exercise.
Recommended Article: How to Create Microservices — A Practical Guide Using Go
Thanks for reading this article. Be sure to share this article if you found it helpful. It would let others get this article and spread the knowledge. Also, putting a clap will motivate me to write more such articles
Find more about me here
Janishar Ali
Nextjs is a very popular framwork for writing API backend services using Typescript (JavaScript). Building a secure and maintainable service requires understanding software concepts, dependency injection, abstraction, and integration with popular libraries. In this article we will deep dive and develop a complete project to power an actual mobile application.

AfterAcademy Tech
In this tutorial, we will learn to build the RESTful API using Node and Express. The goal is to make you comfortable in building your RESTful API with Node.js and Express. At the end of this blog, you will be able to build your REST APIs using Node.js and Express.

AfterAcademy Tech
In this blog, we will learn about API. We will see what exactly is API and by the end of this blog, you will get the knowledge of APIs with the help of some conceptual and practical example.

Janishar Ali
Node.js backend architecture in Typescript. Learn the concepts behind building a highly maintainable and performant backend server application in ExpressJs. Implement a blog platform - role based APIs using JWT. ExpressJs, Mongodb, Redis, Joi, and Jest
