# 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](https://github.com/jkaninda/okapi)—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:

```bash
mkdir okapi-example && cd okapi-example
go mod init okapi-example
go get github.com/jkaninda/okapi@latest
```

Project folder structure:

```yaml
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:

```go
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:

```bash
go run main.go
```

Visit [http://localhost:8080](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`:

```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.

```go

package middlewares

import (
	"errors"
	"fmt"
	"github.com/golang-jwt/jwt/v5"
	"github.com/jkaninda/okapi"
	"github.com/jkaninda/okapi/examples/route-definition/models"
	"log/slog"
	"net/http"
	"strings"
	"time"
)

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(c okapi.Context, claims jwt.Claims) error {
			slog.Info("Validating JWT claims for role using custom function")
			// Simulate a custom claims validation
			mapClaims, ok := claims.(jwt.MapClaims)
			if !ok {
				return errors.New("invalid claims type")
			}
			if role, exist := mapClaims["user"].(map[string]interface{})["role"]; exist {
				fmt.Println("Role from claims:", role)
			}
			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.

```go
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.

```go
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`:

```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

```bash
go run main.go
```

### Access the API

* Visit: [http://localhost:8080](http://localhost:8080/)
    
* Swagger UI: [http://localhost:8080/docs](http://localhost:8080/docs)
    

Example response:

```json
{
  "message": "Welcome to the Okapi Web Framework!"
}
```

---

### Swagger UI

![Okapi OpenAPI Swagger UI](https://cdn.hashnode.com/res/hashnode/image/upload/v1750667316513/de2ed73a-a4c8-46a6-8457-ff4527e057d5.png align="center")

## 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](https://github.com/jkaninda/okapi)

#### Source code: [https://github.com/jkaninda/okapi/tree/main/examples/route-definition](https://github.com/jkaninda/okapi/tree/main/examples/route-definition)

### 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
    

---
