REST API with Golang - up1/workshop-springboot-nodejs-go GitHub Wiki

REST API with Golang

1. Project structure

├── main.go
├── handlers/
│   └── product.go
├── models/
│   └── product.go
├── db/
│   └── db.go
├── redis/
│   └── redis.go

2. Create project

$go mod init demo

3. Create file models/product.go

package models

type Product struct {
	ID       int     `json:"id"`
	Name     string  `json:"name"`
	Code     string  `json:"code"`
	Quantity int     `json:"quantity"`
	Price    float64 `json:"price"`
}

4. Create file db/db.go

  • Create connection to database
package db

import (
	"database/sql"
	"fmt"
	"log"
	"os"

	_ "github.com/joho/godotenv/autoload"
	_ "github.com/lib/pq"
)

var DB *sql.DB

func Init() {
	var err error
	dbHost := os.Getenv("POSTGRES_HOST")
	dbPort := os.Getenv("POSTGRES_PORT")
	dbUser := os.Getenv("POSTGRES_USERNAME")
	dbPassword := os.Getenv("POSTGRES_PASSWORD")
	dbName := os.Getenv("POSTGRES_DATABASE_NAME")

	connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
		dbHost, dbPort, dbUser, dbPassword, dbName)
	DB, err = sql.Open("postgres", connStr)
	if err != nil {
		log.Fatal("PostgreSQL connection error:", err)
	}
	if err = DB.Ping(); err != nil {
		log.Fatal("Unable to reach the database:", err)
	}
}

Install dependencies

$go mod tidy

Create .env file

POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_USERNAME=user01
POSTGRES_PASSWORD=password01
POSTGRES_DATABASE_NAME=demodb

REDIS_HOST=localhost
REDIS_PORT=6379

Create products table

CREATE TABLE products (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100),
    code VARCHAR(50),
    quantity INT,
    price NUMERIC(10, 2)
);

5. Create file redis/redis.go

  • Create connection to redis
package redis

import (
	"context"
	"log"

	"github.com/redis/go-redis/v9"
)

var Rdb *redis.Client
var Ctx = context.Background()

func Init() {
	Rdb = redis.NewClient(&redis.Options{
		Addr: "localhost:6379",
	})
	if err := Rdb.Ping(Ctx).Err(); err != nil {
		log.Fatal("Unable to reach the Redis server:", err)
	}
}

Install dependencies

$go mod tidy

6. Create file handlers/product.go

  • Get product by id
  • Create a new product
package handlers

import (
	"encoding/json"
	"net/http"

	"demo/db"
	"demo/models"
	"demo/redis"

	"github.com/labstack/echo/v4"
)

func GetProductByID(c echo.Context) error {
	id := c.Param("id")

	// Try Redis
	cached, err := redis.Rdb.Get(redis.Ctx, id).Result()
	if err == nil {
		var product models.Product
		json.Unmarshal([]byte(cached), &product)
		return c.JSON(http.StatusOK, product)
	}

	// Query DB
	row := db.DB.QueryRow("SELECT id, name, code, quantity, price FROM products WHERE id = $1", id)
	var product models.Product
	err = row.Scan(&product.ID, &product.Name, &product.Code, &product.Quantity, &product.Price)
	if err != nil {
		return c.JSON(http.StatusNotFound, echo.Map{"error": "Product not found"})
	}

	// Cache result
	data, _ := json.Marshal(product)
	redis.Rdb.Set(redis.Ctx, id, data, 0)

	return c.JSON(http.StatusOK, product)
}

func CreateProduct(c echo.Context) error {
	var p models.Product
	if err := c.Bind(&p); err != nil {
		return c.JSON(http.StatusBadRequest, echo.Map{"error": "Invalid request"})
	}

	err := db.DB.QueryRow(
		"INSERT INTO products(name, code, quantity, price) VALUES($1, $2, $3, $4) RETURNING id",
		p.Name, p.Code, p.Quantity, p.Price,
	).Scan(&p.ID)
	if err != nil {
		return c.JSON(http.StatusInternalServerError, echo.Map{"error": "Failed to insert product"})
	}

	return c.JSON(http.StatusCreated, p)
}

Install dependencies

$go mod tidy

7. Create file main.go

  • Main file of service
package main

import (
	"demo/db"
	"demo/handlers"
	"demo/redis"

	"github.com/labstack/echo/v4"
)

func main() {
	db.Init()
	redis.Init()

	e := echo.New()

	e.GET("/product/:id", handlers.GetProductByID)
	e.POST("/product", handlers.CreateProduct)

	e.Logger.Fatal(e.Start(":8080"))
}

Run

$go run main.go 

8. Working with Structured log

Edit file main.go

package main

import (
	"demo/db"
	"demo/handlers"
	"demo/redis"
	"log/slog"
	"os"

	"github.com/labstack/echo/v4"
	slogecho "github.com/samber/slog-echo"
)

func main() {
	db.Init()
	redis.Init()

	logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
	slog.SetDefault(logger)

	e := echo.New()
	e.Use(slogecho.New(logger))

	e.GET("/product/:id", handlers.GetProductByID)
	e.POST("/product", handlers.CreateProduct)

	e.Logger.Fatal(e.Start(":8080"))
}

Write log

func GetProductByID(c echo.Context) error {
	id := c.Param("id")

	// Write log
	slog.Info("Fetching product", "id", id, "method", c.Request().Method, "path", c.Request().URL.Path)
...

9. Validate input in handler

func CreateProduct(c echo.Context) error {
	var p models.Product
	// Bind request body to product struct
	if err := c.Bind(&p); err != nil {
		return c.JSON(http.StatusBadRequest, echo.Map{"error": "Invalid request"})
	}

	// Define JSON schema for validation
	schema := `{
		"type": "object",
		"required": ["name", "code", "quantity", "price"],
		"properties": {
			"name": {"type": "string", "minLength": 1},
			"code": {"type": "string", "minLength": 1},
			"quantity": {"type": "number", "minimum": 0},
			"price": {"type": "number", "minimum": 0}
		}
	}`

	loader := gojsonschema.NewStringLoader(schema)
	document := gojsonschema.NewGoLoader(p)

	result, err := gojsonschema.Validate(loader, document)
	if err != nil {
		return c.JSON(http.StatusBadRequest, echo.Map{"error": "Schema validation error"})
	}

	if !result.Valid() {
		return c.JSON(http.StatusBadRequest, echo.Map{"error": "Invalid product data"})
	}

	err = db.DB.QueryRow(
		"INSERT INTO products(name, code, quantity, price) VALUES($1, $2, $3, $4) RETURNING id",
		p.Name, p.Code, p.Quantity, p.Price,
	).Scan(&p.ID)
	if err != nil {
		return c.JSON(http.StatusInternalServerError, echo.Map{"error": "Failed to insert product"})
	}

	return c.JSON(http.StatusCreated, p)
}