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
models/product.go
3. Create file 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"`
}
db/db.go
4. Create file - 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)
);
redis/redis.go
5. Create file - 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
handlers/product.go
6. Create file - 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
main.go
7. Create file - 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)
}