Building a REST API with Golang, Gin, and Pocketbase

Building a REST API with Golang, Gin, and Pocketbase

Introduction

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.

Why Choose Gin and Pocketbase?

Gin Framework:

  • Performance: Known for its speed and low latency, making it ideal for high-performance applications.

  • Simplicity and Flexibility: Easy to set up, scale, and maintain.

  • Middleware Support: Built-in middleware enables seamless integration of common features like logging and authentication.

Pocketbase:

  • User Management: Out-of-the-box support for user registration, login, and password reset functionalities.

  • Real-Time Database: Simplifies real-time data management with minimal configuration.

  • Ease of Use: Pocketbase abstracts away many backend complexities, allowing developers to focus on building the core application. _

⚠️ Important Note About Pocketbase:

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

Prerequisites

  1. Golang: Install Go from Go Downloads.

  2. GitHub Account: Required for cloning repositories and managing code.

  3. Pocketbase: Download the latest release from Pocketbase GitHub.

Setting Up the Project Structure

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

Setting Up Pocketbase

Download and Install Pocketbase:

  1. Download the latest release of Pocketbase from the GitHub repository.

  2. Extract the files and place them in your project folder.

  3. To start Pocketbase, run the following command:

     ./pocketbase serve
    

    This will start Pocketbase at http://127.0.0.1:8090.

  4. Navigate to the Pocketbase dashboard at http://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:

Configuring API URLs

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"

Setting Up Golang with Gin

Install Gin:

To install the Gin framework in your Go project, run the following command:

go get -u github.com/gin-gonic/gin

This will install Gin and its dependencies into your Go workspace.

API Endpoints and main.go Implementation

Here 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")
}

Integrating Repository, Service, and Handler

Repository Layer (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 Layer (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 Layer (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
}

Response Utilities (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)
}

JWT Middleware (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()
}

Conclusion

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.

Next Steps

  • Add Password Reset: Implement a password reset flow using JWT for authentication.

  • Use More Pocketbase Features: Pocketbase offers additional real-time features that can be useful for interactive applications.

  • Test the API: Write tests for API endpoints to ensure robustness.

  • Deploy: Deploy the application on platforms like AWS, Google Cloud, or DigitalOcean.

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.