Creating REST APIs in Golang: A Practical Guide with Okapi Framework


REST (Representational State Transfer) APIs have become the backbone of modern web applications, enabling efficient communication and data exchange across systems.
Go (or Golang) is increasingly favored for backend development due to its performance, simplicity, and concurrency features.
In this tutorial, we’ll walk through building a REST API using the Okapi framework—a modern, minimalist web framework for Go that emphasizes developer experience and clean API design.
What is Okapi?
Okapi is a lightweight, performant web framework inspired by FastAPI. It is designed to help developers build scalable, well-documented APIs quickly, with minimal boilerplate.
Key features:
Minimalist syntax: Clean and declarative route definitions.
OpenAPI integration: Automatic and real-time OpenAPI spec generation.
Extensible middleware: Support for custom and built-in middleware like JWTAuth.
Why use Okapi?
Easy to Learn – With familiar Go syntax and intuitive APIs, you can be productive in minutes, even on your first project.
Lightweight and Unopinionated – Okapi is built from the ground up and doesn’t wrap or build on top of another framework. It gives you full control without unnecessary abstraction or bloat.
Highly Flexible – Designed to adapt to your architecture and workflow, not the other way around.
Built for Production – Fast, reliable, and efficient under real-world load. Okapi is optimized for performance without sacrificing developer experience.
Standard Library Compatibility - Integrates seamlessly with Go’s net/http standard library, making it easy to combine Okapi with existing Go code and tools.
Automatic OpenAPI Documentation - Generate comprehensive OpenAPI specs automatically for every route, keeping your API documentation always up to date with your code.
Dynamic Route Management - Enable or disable routes and route groups at runtime. No need to comment out code—just toggle behavior cleanly and efficiently.
Ideal for:
High-performance REST APIs
Composable microservices
Rapid prototyping
Learning & teaching Go web development
Whether you're building your next startup, internal tools, or side projects, Okapi scales with you.
Prerequisites
Before you begin, ensure you have the following installed:
Basic knowledge of Go
Go installed (v1.24+ recommended)
A code editor (VS Code or similar)
Project Setup
Start by creating your project directory and initializing Go modules:
mkdir okapi-example && cd okapi-example
go mod init okapi-example
go get github.com/jkaninda/okapi@latest
Project folder structure:
okapi-example/
├── controllers
│ └── controller.go
├── go.mod
├── go.sum
├── main.go
├── middlewares
│ └── middleware.go
├── models
│ └── model.go
└── routes
└── route.go
Basic Routing Example
Here’s a minimal Okapi server that responds with a welcome message:
package main
import (
"github.com/jkaninda/okapi"
)
func main() {
o := okapi.Default()
o.Get("/", func(c okapi.Context) error {
return c.OK(okapi.M{"message": "Hello from Okapi Web Framework!", "License": "MIT"})
})
o.Get("/greet/:name", func(c okapi.Context) error {
name := c.Param("name")
return c.OK(okapi.M{"message": "Hello, " + name + "!"})
})
if err := o.Start(); err != nil {
panic(err)
}
}
Run it with:
go run main.go
Visit http://localhost:8080 to see the response.
Building a Book API
We’ll now create a simple API to manage books, including authentication for admin-level routes.
1. Define Models
models/model.go
:
package models
type Response struct {
Success bool `json:"success"`
Message string `json:"message"`
Data Book `json:"data"`
}
type Book struct {
Id int `json:"id"`
Name string `json:"name" form:"name" max:"50" required:"true" description:"Book name"`
Price int `json:"price" form:"price" query:"price" yaml:"price" required:"true" description:"Book price"`
}
type ErrorResponse struct {
Success bool `json:"success"`
Status int `json:"status"`
Details any `json:"details"`
}
type AuthRequest struct {
Username string `json:"username" required:"true" description:"Username for authentication"`
Password string `json:"password" required:"true" description:"Password for authentication"`
}
type AuthResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
Token string `json:"token,omitempty"`
ExpiresAt int64 `json:"expires,omitempty"`
}
type UserInfo struct {
Name string `json:"name"`
Email string `json:"email"`
Role string `json:"role"`
}
2. Implement Middleware (JWT)
middlewares/middleware.go
:
Uses Okapi's built-in JWTAuth with claim validation and token generation.
package middlewares
import (
"fmt"
"log/slog"
"net/http"
"okapi-example/models"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/jkaninda/okapi"
)
var (
// signingSecret is used to sign the JWT tokens
signingSecret = "supersecret"
JWTAuth = &okapi.JWTAuth{
SigningSecret: []byte(signingSecret),
TokenLookup: "header:Authorization",
ClaimsExpression: "Equals(`email_verified`, `true`) && Equals(`user.role`, `admin`) && Contains(`permissions`, `create`, `delete`, `update`)",
ForwardClaims: map[string]string{
"email": "user.email",
"role": "user.role",
"name": "user.name",
},
// CustomClaims claims validation function
ValidateClaims: func(claims jwt.Claims) error {
slog.Info("Validating JWT claims for role using custom function")
// Simulate a custom claims validation
if _, ok := claims.(jwt.Claims); ok {
}
return nil
},
}
jwtClaims = jwt.MapClaims{
"sub": "12345",
"iss": "okapi.example.com",
"aud": "okapi.example.com",
"user": map[string]string{
"name": "",
"role": "",
"email": "",
},
"email_verified": true,
"permissions": []string{"read", "create"},
"exp": time.Now().Add(2 * time.Hour).Unix(),
}
adminPermissions = []string{"read", "create", "delete", "update"}
)
func Login(authRequest *models.AuthRequest) (models.AuthResponse, error) {
// This is where you would typically validate the user credentials against a database
slog.Info("Login attempt", "username", authRequest.Username)
// Simulate a login function that returns a JWT token
if authRequest.Username != "admin" && authRequest.Password != "password" ||
authRequest.Username != "user" && authRequest.Password != "password" {
return models.AuthResponse{
Success: false,
Message: "Invalid username or password",
}, fmt.Errorf("username or password is wrong")
}
if _, ok := jwtClaims["user"].(map[string]string); ok {
jwtClaims["user"].(map[string]string)["name"] = strings.ToUpper(authRequest.Username)
jwtClaims["user"].(map[string]string)["role"] = authRequest.Username
jwtClaims["user"].(map[string]string)["email"] = authRequest.Username + "@example.com"
jwtClaims["permissions"] = []string{"read"}
// If the user is an admin, add admin permissions
if authRequest.Username == "admin" {
jwtClaims["permissions"] = adminPermissions
}
}
// Set the expiration time for the JWT token
expireAt := 30 * time.Minute
jwtClaims["exp"] = time.Now().Add(expireAt).Unix()
token, err := okapi.GenerateJwtToken(JWTAuth.SigningSecret, jwtClaims, expireAt)
if err != nil {
return models.AuthResponse{
Success: false,
Message: "Invalid username or password",
}, fmt.Errorf("failed to generate JWT token: %w", err)
}
return models.AuthResponse{
Success: true,
Message: "Welcome back " + authRequest.Username,
Token: token,
ExpiresAt: time.Now().Add(expireAt).Unix(),
}, nil
}
func CustomMiddleware(next okapi.HandleFunc) okapi.HandleFunc {
return func(c okapi.Context) error {
slog.Info("Custom middleware executed", "path", c.Request().URL.Path, "method", c.Request().Method)
// You can add any custom logic here, such as logging, authentication, etc.
// For example, let's log the request method and URL
slog.Info("Request received", "method", c.Request().Method, "url", c.Request().URL.String())
// Call the next handler in the chain
if err := next(c); err != nil {
// If an error occurs, log it and return a generic error response
slog.Error("Error in custom middleware", "error", err)
return c.JSON(http.StatusInternalServerError, okapi.M{"error": "Internal Server Error"})
}
return nil
}
}
3. Create Controllers
controllers/controller.go
:
Implements handlers for Home, Book, and Authentication logic.
package controllers
import (
"fmt"
"github.com/jkaninda/okapi"
"net/http"
"okapi-example/middlewares"
"okapi-example/models"
"strconv"
)
type BookController struct{}
type CommonController struct{}
type AuthController struct{}
var (
books = []*models.Book{
{Id: 1, Name: "The Go Programming Language ", Price: 100},
{Id: 2, Name: "Building REST/API With Okapi ", Price: 50},
{Id: 3, Name: "Learning Go", Price: 200},
{Id: 4, Name: "Go Web Programming", Price: 300},
{Id: 5, Name: "Go in Action", Price: 150},
}
ApiVersion = "V1"
)
// ****************** Controllers *****************
func (hc *CommonController) Home(c okapi.Context) error {
return c.OK(okapi.M{"message": "Welcome to the Okapi Web Framework!"})
}
func (hc *CommonController) Version(c okapi.Context) error {
return c.OK(okapi.M{"version": ApiVersion})
}
func (bc *BookController) GetBooks(c okapi.Context) error {
// Simulate fetching books from a database
return c.OK(books)
}
func (bc *BookController) CreateBook(c okapi.Context) error {
// Simulate creating a book in a database
book := &models.Book{}
err := c.Bind(book)
if err != nil {
return c.ErrorBadRequest(models.ErrorResponse{Success: false, Status: http.StatusBadRequest, Details: err.Error()})
}
book.Id = len(books) + 1
books = append(books, book)
response := models.Response{
Success: true,
Message: "Book created successfully",
Data: *book,
}
return c.OK(response)
}
func (bc *BookController) GetBook(c okapi.Context) error {
id := c.Param("id")
i, err := strconv.Atoi(id)
if err != nil {
return c.ErrorBadRequest(models.ErrorResponse{Success: false, Status: http.StatusBadRequest, Details: err.Error()})
}
// Simulate a fetching book from a database
for _, book := range books {
if book.Id == i {
return c.OK(book)
}
}
return c.AbortNotFound("Book not found")
}
func (bc *BookController) DeleteBook(c okapi.Context) error {
id := c.Param("id")
i, err := strconv.Atoi(id)
if err != nil {
return c.ErrorBadRequest(models.ErrorResponse{Success: false, Status: http.StatusBadRequest, Details: err.Error()})
}
// Simulate deleting a book from a database
for index, book := range books {
if book.Id == i {
books = append(books[:index], books[index+1:]...)
return c.OK(models.Response{
Success: true,
Message: "Book deleted successfully",
})
}
}
return c.AbortNotFound("Book not found")
}
// ******************** AuthController *****************
func (bc *AuthController) Login(c okapi.Context) error {
authRequest := &models.AuthRequest{}
err := c.Bind(authRequest)
if err != nil {
return c.ErrorBadRequest(models.ErrorResponse{Success: false, Status: http.StatusBadRequest, Details: err.Error()})
}
// Validate the authRequest and generate a JWT token
authResponse, err := middlewares.Login(authRequest)
if err != nil {
return c.ErrorUnauthorized(authResponse)
}
return c.OK(authResponse)
}
func (bc *AuthController) WhoAmI(c okapi.Context) error {
//Get User Information from the context, shared by the JWT middleware using forwardClaims
email := c.GetString("email")
if email == "" {
return c.AbortUnauthorized("Unauthorized", fmt.Errorf("user not authenticated"))
}
// Respond with the current user information
return c.OK(models.UserInfo{
Email: email,
Role: c.GetString("role"),
Name: c.GetString("name"),
},
)
}
4. Define Routes
routes/route.go
:
Group routes by feature and use route definitions with OpenAPI documentation metadata.
package routes
import (
"net/http"
"okapi-example/controllers"
"okapi-example/middlewares"
"okapi-example/models"
"github.com/jkaninda/okapi"
)
// ****************** Controllers ******************
var (
bookController = &controllers.BookController{}
commonController = &controllers.CommonController{}
authController = &controllers.AuthController{}
)
type Route struct {
// app is the Okapi application
app *okapi.Okapi
}
// NewRoute creates a new Route instance with the Okapi application
func NewRoute(app *okapi.Okapi) *Route {
// Update OpenAPI documentation with the application title and version
app.WithOpenAPIDocs(okapi.OpenAPI{
Title: "REST APIs with Okapi Framework",
Version: controllers.ApiVersion,
Licence: okapi.License{
Name: "MIT",
URL: "https://opensource.org/license/mit/",
},
})
return &Route{
app: app,
}
}
// ****************** Routes Definition ******************
// Home returns the route definition for the Home endpoint
func (r *Route) Home() okapi.RouteDefinition {
return okapi.RouteDefinition{
Path: "/",
Method: http.MethodGet,
Handler: commonController.Home,
Group: &okapi.Group{Prefix: "/", Tags: []string{"CommonController"}},
Options: []okapi.RouteOption{
okapi.DocSummary("Home"),
okapi.DocDescription("Welcome to the Okapi Web Framework!"),
},
}
}
// Version returns the route definition for the Version endpoint
func (r *Route) Version() okapi.RouteDefinition {
return okapi.RouteDefinition{
Path: "/version",
Method: http.MethodGet,
Handler: commonController.Version,
Group: &okapi.Group{Prefix: "/api/v1", Tags: []string{"CommonController"}},
Options: []okapi.RouteOption{
okapi.DocSummary("API Version"),
okapi.DocDescription("Get the API version"),
okapi.DocResponse(okapi.M{"version": "v1"}),
},
}
}
// ************* Book Routes *************
// In this section, we will make BookRoutes deprecated and create BookV1Routes
// BookRoutes returns the route definitions for the BookController
func (r *Route) BookRoutes() []okapi.RouteDefinition {
apiGroup := &okapi.Group{Prefix: "/api", Tags: []string{"BookController"}}
// Mark the group as deprecated
// But, it will still be available for use, it's just marked as deprecated on the OpenAPI documentation
apiGroup.Deprecated()
// Apply custom middleware
apiGroup.Use(middlewares.CustomMiddleware)
return []okapi.RouteDefinition{
{
Method: http.MethodGet,
Path: "/books",
Handler: bookController.GetBooks,
Group: apiGroup,
Options: []okapi.RouteOption{
okapi.DocSummary("Get Books"),
okapi.DocDescription("Retrieve a list of books"),
okapi.DocResponse([]models.Book{}),
okapi.DocResponse(http.StatusBadRequest, models.ErrorResponse{}),
okapi.DocResponse(http.StatusNotFound, models.ErrorResponse{}),
},
},
{
Method: http.MethodGet,
Path: "/books/:id",
Handler: bookController.GetBook,
Group: apiGroup,
Options: []okapi.RouteOption{
okapi.DocSummary("Get Book by ID"),
okapi.DocDescription("Retrieve a book by its ID"),
okapi.DocPathParam("id", "int", "The ID of the book"),
okapi.DocResponse(models.Book{}),
okapi.DocResponse(http.StatusBadRequest, models.ErrorResponse{}),
okapi.DocResponse(http.StatusNotFound, models.ErrorResponse{}),
},
},
}
}
// *************** End of Book Routes ***************
// *********************** Book v1 Routes ***********************
func (r *Route) V1BookRoutes() []okapi.RouteDefinition {
apiGroup := &okapi.Group{Prefix: "/api"}
apiV1Group := apiGroup.Group("/v1").WithTags([]string{"BookController"})
// Apply custom middleware
apiGroup.Use(middlewares.CustomMiddleware)
return []okapi.RouteDefinition{
{
Method: http.MethodGet,
Path: "/books",
Handler: bookController.GetBooks,
Group: apiV1Group,
Options: []okapi.RouteOption{
okapi.DocSummary("Get Books"),
okapi.DocDescription("Retrieve a list of books"),
okapi.DocResponse([]models.Book{}),
okapi.DocResponse(http.StatusBadRequest, models.ErrorResponse{}),
},
},
{
Method: http.MethodGet,
Path: "/books/:id",
Handler: bookController.GetBook,
Group: apiV1Group,
Options: []okapi.RouteOption{
okapi.DocSummary("Get Book by ID"),
okapi.DocDescription("Retrieve a book by its ID"),
okapi.DocPathParam("id", "int", "The ID of the book"),
okapi.DocResponse(models.Book{}),
okapi.DocResponse(http.StatusBadRequest, models.ErrorResponse{}),
okapi.DocResponse(http.StatusNotFound, models.ErrorResponse{}),
},
},
}
}
// *************** Auth Routes ****************
func (r *Route) AuthRoute() okapi.RouteDefinition {
apiGroup := &okapi.Group{Prefix: "/api/v1/auth", Tags: []string{"AuthController"}}
apiGroup.Use(middlewares.CustomMiddleware)
return okapi.RouteDefinition{
Method: http.MethodPost,
Path: "/login",
Handler: authController.Login,
Group: apiGroup,
Options: []okapi.RouteOption{
okapi.DocSummary("Login"),
okapi.DocDescription("User login to get a JWT token"),
okapi.DocRequestBody(models.AuthRequest{}),
okapi.DocResponse(models.AuthResponse{}),
okapi.DocResponse(http.StatusUnauthorized, models.AuthResponse{}),
},
}
}
// ************** Authenticated Routes **************
func (r *Route) SecurityRoutes() []okapi.RouteDefinition {
coreGroup := &okapi.Group{Prefix: "/api/v1/security", Tags: []string{"SecurityController"}}
// Apply JWT authentication middleware to the admin group
coreGroup.Use(middlewares.JWTAuth.Middleware)
// Apply custom middleware
coreGroup.Use(middlewares.CustomMiddleware)
coreGroup.WithBearerAuth() //Enable Bearer token for OpenAPI documentation
return []okapi.RouteDefinition{
{
Method: http.MethodPost,
Path: "/whoami",
Handler: authController.WhoAmI,
Group: coreGroup,
Options: []okapi.RouteOption{
okapi.DocSummary("Whoami"),
okapi.DocDescription("Get the current user's information"),
okapi.DocResponse(models.UserInfo{}),
},
},
}
}
// ***************** Admin Routes *****************
func (r *Route) AdminRoutes() []okapi.RouteDefinition {
apiGroup := &okapi.Group{Prefix: "/api/v1/admin", Tags: []string{"AdminController"}}
// Apply JWT authentication middleware to the admin group
apiGroup.Use(middlewares.JWTAuth.Middleware)
apiGroup.Use(middlewares.CustomMiddleware)
apiGroup.WithBearerAuth() //Enable Bearer token for OpenAPI documentation
return []okapi.RouteDefinition{
{
Method: http.MethodPost,
Path: "/books",
Handler: bookController.CreateBook,
Group: apiGroup,
Options: []okapi.RouteOption{
okapi.DocSummary("Create Book"),
okapi.DocDescription("Create a new book"),
okapi.DocRequestBody(models.Book{}),
okapi.DocResponse(models.Response{}),
},
},
{
Method: http.MethodDelete,
Path: "/books/:id",
Handler: bookController.DeleteBook,
Group: apiGroup,
Options: []okapi.RouteOption{
okapi.DocSummary("Delete Book by ID"),
okapi.DocDescription("Delete a book by its ID"),
okapi.DocPathParam("id", "int", "The ID of the book"),
okapi.DocResponse(models.Response{}),
okapi.DocResponse(http.StatusNotFound, models.ErrorResponse{}),
okapi.DocResponse(http.StatusUnauthorized, models.ErrorResponse{}),
},
},
}
}
5. Main Entry Point
main.go
:
package main
import (
"github.com/jkaninda/okapi"
"okapi-example/routes"
)
func main() {
app := okapi.Default()
// Create the route instance
route := routes.NewRoute(app)
// Register routes
app.Register(route.Home())
app.Register(route.Version())
app.Register(route.AdminRoutes()...)
app.Register(route.AuthRoute())
app.Register(route.SecurityRoutes()...)
app.Register(route.BookRoutes()...)
app.Register(route.V1BookRoutes()...)
// Start the server
if err := app.Start(); err != nil {
panic(err)
}
}
Run the Server
go run main.go
Access the API
Visit: http://localhost:8080
Swagger UI: http://localhost:8080/docs
Example response:
{
"message": "Welcome to the Okapi Web Framework!"
}
Swagger UI
Summary
With Okapi, you can create well-structured, RESTful APIs in Go with minimal effort. Features like declarative routing, built-in JWT authentication, and automatic OpenAPI documentation make it ideal for modern backend development.
Okapi Github: https://github.com/jkaninda/okapi
Github:
Next Steps
Add database integration (e.g., PostgreSQL with GORM)
Integrate unit tests
Deploy to Docker or cloud platforms
Secure your JWT secrets using environment variables
Subscribe to my newsletter
Read articles from Jonas Kaninda directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Jonas Kaninda
Jonas Kaninda
I’m a Software Engineer with over 5 years of hands-on experience building scalable, reliable systems. My expertise spans Kotlin, Spring Boot, Go (Golang), MySQL, PostgreSQL, and Linux systems, with a strong focus on DevOps, Docker, and Kubernetes. Programming is more than a job, it's my passion. I’m driven by curiosity, constantly exploring new tools, frameworks, and architectural patterns to stay at the forefront of the tech landscape. I'm a strong advocate of Open Source and enjoy building tools that simplify infrastructure and developer workflows. My OSS contributions include solutions for database backup/migration, encryption, API Gateway management, and Kubernetes Operators. My current interests revolve around: Cloud-native architecture Microservices and API frameworks DevOps & GitOps practices SRE and DevSecOps principles I bring experience in designing, deploying, and maintaining modern software systems, with a deep understanding of CI/CD pipelines, infrastructure automation, and container orchestration. I thrive in both collaborative environments and independent work—always aiming for clean, maintainable code and high-impact solutions.