Building a REST API with Golang, Gin, and Pocketbase
Rowland Adimoha / January 21, 2025
8 min read
Rowland Adimoha / January 21, 2025
8 min read

Building a REST API with modern technologies can be an engaging yet complex process, especially when you are working with unfamiliar stacks. As a Go developer, I decided to explore the combination of Golang with the Gin framework and Pocketbase for building a REST API. This choice was driven by a desire to experiment with a different tech stack while also considering the positive feedback I had heard from the developer community regarding Pocketbase's user-friendly features.
We’ll walk you through setting up the environment, configuring the API routes, and creating a service layer to interact with Pocketbase for user authentication and data management. By the end of this tutorial, you will have a fully functional REST API capable of user registration and login.
Pocketbase is under active development and may undergo breaking changes before version 1.0.0. It is not recommended for production use unless you are comfortable with frequent updates and manual migrations.
The directory structure of the project is as follows:
walkit/
├── .github/
├── cmd/
│ └── server/
├── config/
├── internal/
│ ├── middleware/
│ ├── handler/
│ ├── model/
│ ├── repository/
│ ├── service/
│ ├── routes/
├── pb_data/
├── pkg/
│ ├── util/
│ └── logger/
├── air.toml
├── .env
├── Dockerfile
├── pocketbase
├── Makefile
├── go.mod
├── go.sum
└── README.mdhttp://127.0.0.1:8090../pocketbase servehttp://127.0.0.1:8090/_/ and set up a root password for the admin interface.Once the server is running, Pocketbase provides two essential endpoints:
http://127.0.0.1:8090/api/http://127.0.0.1:8090/_/In the config.go file, we'll use Viper to manage and load configuration settings, such as the Pocketbase API URL and JWT secret. This ensures that the Golang application can securely interact with Pocketbase for user authentication and management.
package config
import (
"log"
"github.com/spf13/viper"
)
type Config struct {
BaseURL string
JWTSecret string
Environment string
CORSAllowedOrigins []string
Port string
}
func LoadConfig() *Config {
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath("./config")
viper.AddConfigPath(".")
viper.AutomaticEnv()
viper.SetDefault("cors_allowed_origins", []string{"*"})
if err := viper.ReadInConfig(); err != nil {
log.Printf("Error reading config file, using defaults: %v", err)
}
corsAllowedOrigins := viper.GetStringSlice("cors_allowed_origins")
if len(corsAllowedOrigins) == 0 {
corsAllowedOrigins = []string{"*"}
}
return &Config{
BaseURL: viper.GetString("pocket_base_url"),
JWTSecret: viper.GetString("jwt_secret"),
Environment: viper.GetString("app_env"),
CORSAllowedOrigins: corsAllowedOrigins,
Port: viper.GetString("port"),
}
}The config.yml file should be structured as follows:
pocket_base_url: "http://127.0.0.1:8090/api"
jwt_secret: "your_jwt_secret_key"
app_env: "development"
cors_allowed_origins:
- "*"
port: "8080"To install the Gin framework in your Go project, run the following command:
go get -u github.com/gin-gonic/ginThis will install Gin and its dependencies into your Go workspace.
main.go ImplementationHere are the two primary API endpoints for registering and logging in users:
POST /api/v1/auth/register – Allows a new user to register.POST /api/v1/auth/login – Allows a registered user to log in and receive a JWT token.package main
import (
"context"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"github.com/rowjay007/walkit/config"
"github.com/rowjay007/walkit/internal/routes"
"github.com/rowjay007/walkit/pkg/logger"
)
func main() {
logger := logger.New()
cfg := config.LoadConfig()
if cfg.Environment == "production" {
gin.SetMode(gin.ReleaseMode)
}
router := gin.New()
router.Use(gin.Recovery())
corsConfig := cors.DefaultConfig()
corsConfig.AllowOrigins = cfg.CORSAllowedOrigins
router.Use(cors.New(corsConfig))
routes.LoadRoutes(router)
srv := &http.Server{
Addr: ":" + cfg.Port,
Handler: router,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
go func() {
logger.Info("Starting server on port " + cfg.Port)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.Fatal("Failed to start server: " + err.Error())
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
logger.Info("Shutting down server...")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
logger.Fatal("Server forced to shutdown: " + err.Error())
}
logger.Info("Server exiting")
}
repository.go):This layer interacts directly with Pocketbase for data management, such as registering users or logging them in.
package repository
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"github.com/rowjay007/walkit/config"
"github.com/rowjay007/walkit/internal/model"
)
func AuthAPI() string {
return config.LoadConfig().BaseURL + "/collections/users"
}
func RegisterUser(user model.User) error {
userJSON, err := json.Marshal(user)
if err != nil {
return fmt.Errorf("error marshaling user data: %w", err)
}
resp, err := http.Post(AuthAPI(), "application/json", bytes.NewBuffer(userJSON))
if err != nil {
return fmt.Errorf("error making request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
return fmt.Errorf("registration failed: %v", resp.Status)
}
return nil
}
func LoginUser(login model.LoginRequest) (*model.LoginResponse, error) {
loginJSON, err := json.Marshal(login)
if err != nil {
return nil, fmt.Errorf("error marshaling login data: %w", err)
}
req, err := http.NewRequest("POST", AuthAPI()+"/auth-with-password", bytes.NewBuffer(loginJSON))
if err != nil {
return
nil, fmt.Errorf("error creating request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("error making request: %w", err)
}
defer resp.Body.Close()
var response model.LoginResponse
// Handle response decoding here
return &response, nil
}service.go):This layer handles business logic and interacts with the repository.
package service
import (
"github.com/rowjay007/walkit/internal/model"
"github.com/rowjay007/walkit/internal/repository"
)
func LoginUser(login model.LoginRequest) (*model.LoginResponse, error) {
return repository.LoginUser(login)
}
func RegisterUser(user model.User) error {
return repository.RegisterUser(user)
}handler.go):This layer handles HTTP requests and responses.
package handler
import (
"fmt"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/rowjay007/walkit/internal/model"
"github.com/rowjay007/walkit/internal/service"
"github.com/rowjay007/walkit/pkg/util"
)
func LoginUser(c *gin.Context) {
var login model.LoginRequest
if err := c.ShouldBindJSON(&login); err != nil {
util.RespondWithError(c, http.StatusBadRequest, fmt.Sprintf("Invalid request payload: %v", err))
return
}
if err := validateLoginInput(login); err != nil {
util.RespondWithError(c, http.StatusBadRequest, err.Error())
return
}
response, err := service.LoginUser(login)
if err != nil {
statusCode := determineStatusCode(err)
util.RespondWithError(c, statusCode, "Login failed. Please check your credentials.")
return
}
util.RespondWithJSON(c, http.StatusOK, response)
}
func validateLoginInput(login model.LoginRequest) error {
if strings.TrimSpace(login.Identity) == "" {
return fmt.Errorf("identity cannot be empty")
}
if len(login.Password) < 8 {
return fmt.Errorf("password must be at least 8 characters")
}
return nil
}
func determineStatusCode(err error) int {
if strings.Contains(strings.ToLower(err.Error()), "invalid credentials") {
return http.StatusUnauthorized
}
if strings.Contains(strings.ToLower(err.Error()), "not found") {
return http.StatusNotFound
}
return http.StatusInternalServerError
}util.go):This file provides functions to send consistent responses.
package util
import (
"github.com/gin-gonic/gin"
)
func RespondWithError(c *gin.Context, code int, message string) {
c.JSON(code, gin.H{"error": message})
}
func RespondWithJSON(c *gin.Context, code int, payload interface{}) {
c.JSON(code, payload)
}middleware.go):This middleware ensures that a valid JWT token is included in the request header.
package middleware
import (
"fmt"
"net/http"
"strings"
"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
"github.com/rowjay007/walkit/config"
)
func JWTAuthMiddleware(c *gin.Context) {
tokenString := c.GetHeader("Authorization")
if tokenString == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization token is required"})
c.Abort()
return
}
tokenString = strings.TrimPrefix(tokenString, "Bearer ")
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(config.LoadConfig().JWTSecret), nil
})
if err != nil || !token.Valid {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid or expired token"})
c.Abort()
return
}
c.Next()
}Here’s the updated conclusion with a link to the complete code:
In this tutorial, we’ve built a REST API using Golang, Gin, and Pocketbase. The guide covered everything from project structure to implementing user authentication with JWT tokens. We also demonstrated how to integrate multiple layers (repository, service, handler) for better modularity and scalability.
With this setup, you can start extending your API with more features, such as user profile management, JWT token refresh, and other advanced functionalities.
By following this tutorial, you've established a solid foundation for building secure and scalable APIs using Golang, Gin, and Pocketbase.
You can find the complete code for this project on GitHub here.