10 min read

Introducing go-starter-api: A Production-Ready REST API Boilerplate in Go with MVC, JWT, and Dynamic RBAC

Discover how I built 'go-starter-api', a robust Go boilerplate for REST APIs featuring MVC architecture, PostgreSQL with GORM, JWT authentication, dynamic role-based access control, and more, complete with Docker setup and best practices.

GoREST APIMVCJWTRBACPostgreSQLGORMDocker

As a developer exploring Go for backend services, I wanted a solid starter kit for building production-ready REST APIs. That's why I created 'go-starter-api', a boilerplate that incorporates MVC architecture, JWT-based authentication, role-based access control (RBAC) with dynamic permissions, PostgreSQL integration via GORM, file uploads, and Docker support. In this post, I’ll walk you through what I built, highlighting the folder structure and the custom permission handler that makes RBAC seamless and extensible.

This project is open-source and available on GitHub at this Github repo . It's designed to help you get up and running quickly while following Go best practices. Let’s dive in!

Why I Built go-starter-api

Go is fantastic for scalable APIs because of its performance and simplicity, but setting up authentication, authorization, and database handling from scratch can be time-consuming. I wanted to solve that problem by providing:

  • MVC pattern for clean separation of concerns.
  • JWT authentication with access and refresh tokens.
  • Dynamic RBAC where permissions are auto-generated from routes.
  • CRUD operations for users, roles, and permissions.
  • File upload endpoint.
  • PostgreSQL with GORM ORM and auto-migrations.
  • Docker and docker-compose for easy deployment.
  • Logging, middleware, and environment config via .env.

This setup is perfect for kicking off new projects without reinventing the wheel.

Project Folder Structure

One of the key aspects of 'go-starter-api' is its organized structure, which follows Go's standard layout while emphasizing MVC. Here’s the breakdown:

plaintext
go-starter-api/
├── cmd/
│   └── main.go  # Application entry point: loads config, connects DB, runs migrations, seeds data, sets up routes, and starts server
├── config/
│   └── config.go  # Handles loading .env variables for DB, JWT, etc.
├── controllers/
│   ├── auth_controller.go  # Login and token refresh
│   ├── file_controller.go  # File upload handling
│   ├── permission_controller.go  # CRUD for permissions
│   ├── role_controller.go  # CRUD for roles
│   └── user_controller.go  # CRUD for users
├── middlewares/
│   ├── auth_middleware.go  # JWT validation
│   ├── logger_middleware.go  # Request logging
│   └── permission_middleware.go  # RBAC checks
├── models/
│   ├── permission.go
│   ├── role.go
│   └── user.go
├── routes/
│   └── routes.go  # Gin router setup with custom permission registration
├── services/
│   ├── auth_service.go  # JWT logic
│   ├── file_service.go  # File operations
│   ├── permission_service.go  # Permission CRUD and auto-creation
│   ├── role_service.go  # Role CRUD and assignments
│   └── user_service.go  # User CRUD with hashing
├── uploads/  # For stored files (gitignored)
├── .env
├── .gitignore
├── Dockerfile
├── Makefile
├── docker-compose.yml
├── go.mod
├── go.sum
└── README.md

This structure keeps things modular: models define database schemas, services handle business logic, controllers manage HTTP responses, and middlewares enforce security. The 'routes' package is particularly useful for handling dynamic permissions.

The Custom Permission Handler

A standout feature is the dynamic RBAC system. Instead of hardcoding permissions, I auto-generate them based on API endpoints during startup. This idea was inspired by decorator patterns in frameworks like NestJS, but I adapted it for Go.

How It Works

In routes/routes.go, I define custom registration functions (RegisterPOST, RegisterGET, etc.) that collect permissions into a map while attaching the permission middleware:

go
var requiredPermissions = make(map[string]bool)

func RegisterPOST(r gin.IRoutes, relativePath string, permission string, roleSvc *services.RoleService, handlers ...gin.HandlerFunc) {
  requiredPermissions[permission] = true
  handlers = append([]gin.HandlerFunc{middlewares.RequirePermission(permission, roleSvc)}, handlers...)
  r.POST(relativePath, handlers...)
}
// Similar for GET, PUT, DELETE

When setting up routes, I pass the permission string:

go
userCtrl := controllers.NewUserController(userSvc)
RegisterPOST(api, "/users", "create.user", roleSvc, userCtrl.CreateUser)

In main.go, after setup, I create missing permissions in the database:

go
perms := routes.GetCollectedPermissions()
for _, p := range perms {
  if err := permSvc.CreateIfNotExists(p); err != nil {
    log.Fatal().Err(err).Msgf("Failed to auto-create permission %s", p)
  }
}

The permission middleware checks the user’s role against the required permission:

go
func RequirePermission(permission string, roleSvc *services.RoleService) gin.HandlerFunc {
  return func(c gin.Context) {
    roleID := c.GetUint("roleID")
    role, err := roleSvc.GetRoleByID(roleID)
    if err != nil { /* handle error */ }
    hasPerm := false
    for _, p := range role.Permissions {
      if p.Name == permission {
        hasPerm = true
        break
      }
    }
    if !hasPerm { /* 403 Forbidden */ }
    c.Next()
  }
}

On startup, I also seed an 'admin' role with all permissions and an admin user for initial access.

Other Key Features

I use GORM for ORM and auto-migrations from models, so there’s no need for physical SQL files. JWT handles authentication, with claims storing user and role IDs. File uploads save to a server directory, and everything runs in Docker with persistent Postgres data.

Getting Started

Clone the repo, set up .env, and run with docker-compose up. Use the admin credentials to log in and explore.

bash
git clone https://github.com/penealfa/go-starter-api.git
cd go-starter-api
cp .env.example .env
docker-compose up

Conclusion

'go-starter-api' makes it easier to build secure, scalable APIs in Go. The dynamic permission system and clean structure make it extensible for real-world apps. If you try it out or have questions, feel free to reach out on GitHub or my socials. Happy coding!