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.
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:
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.mdThis 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:
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, DELETEWhen setting up routes, I pass the permission string:
userCtrl := controllers.NewUserController(userSvc)
RegisterPOST(api, "/users", "create.user", roleSvc, userCtrl.CreateUser)In main.go, after setup, I create missing permissions in the database:
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:
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.
git clone https://github.com/penealfa/go-starter-api.git
cd go-starter-api
cp .env.example .env
docker-compose upConclusion
'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!