Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions go-fiber/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
FROM golang:1.21-alpine AS builder

WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o main ./cmd/server

FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/

COPY --from=builder /app/main .
EXPOSE 8080
CMD ["./main"]
78 changes: 78 additions & 0 deletions go-fiber/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Product Management API

A high-performance REST API built with Go Fiber, PostgreSQL, and Redis for managing products with advanced features like ratings, tags, analytics, and shopping carts.

## Features

- **Product Management**: CRUD operations for products with metadata support
- **Rating System**: Rate products and get rating summaries
- **Tagging System**: Add tags to products and search by tags
- **Shopping Cart**: Redis-based cart management
- **Analytics**: Activity logging, visitor tracking, and leaderboards
- **Performance**: PostgreSQL for persistence, Redis for caching and analytics

## Quick Start

### Using Docker Compose

1. Clone the repository
2. Run: `docker-compose up -d`
3. API will be available at http://localhost:8080

### Manual Setup

1. Install dependencies: `go mod tidy`
2. Set environment variables:
```bash
export DATABASE_URL="postgres://user:pass@localhost/dbname?sslmode=disable"
export REDIS_URL="redis://localhost:6379"
export PORT="8080"
```
3. Run: `go run cmd/server/main.go`

## API Endpoints

### Products
- `GET /api/v1/products` - List all products
- `POST /api/v1/products` - Create a product
- `GET /api/v1/products/:id` - Get a product
- `PUT /api/v1/products/:id` - Update a product
- `DELETE /api/v1/products/:id` - Delete a product
- `POST /api/v1/products/bulk` - Bulk create products

### Ratings
- `POST /api/v1/products/:id/rate` - Rate a product
- `GET /api/v1/products/:id/ratings` - Get product ratings

### Tags
- `POST /api/v1/products/:id/tags` - Add tags to product
- `GET /api/v1/products/:id/tags` - Get product tags
- `GET /api/v1/tags/:tag/products` - Get products by tag

### Cart
- `POST /api/v1/carts/:userId` - Update user cart
- `GET /api/v1/carts/:userId` - Get user cart

### Analytics
- `GET /api/v1/activity` - Get activity log
- `GET /api/v1/products/:id/visitors` - Get visitor count
- `GET /api/v1/leaderboard` - Get sales leaderboard

### Health
- `GET /health` - Health check

## Architecture

- **Clean Architecture**: Separation of concerns with handlers, services, and repositories
- **PostgreSQL**: Primary database for persistent data
- **Redis**: Caching, sessions, analytics, and real-time features
- **Fiber**: High-performance HTTP framework
- **UUID**: For all entity identifiers

## Performance Features

- Connection pooling for both PostgreSQL and Redis
- Prepared statements for database queries
- Pipeline operations for Redis bulk operations
- Proper indexing on frequently queried columns
- Efficient JSON handling for metadata
69 changes: 69 additions & 0 deletions go-fiber/cmd/server/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package main

import (
"log"
"os"
"os/signal"
"syscall"

"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/gofiber/fiber/v2/middleware/recover"

"your-project/internal/config"
"your-project/internal/database"
"your-project/internal/routes"
)

func main() {
// Load configuration
cfg := config.Load()

// Initialize databases
db, err := database.InitPostgres(cfg.DatabaseURL)
if err != nil {
log.Fatal("Failed to connect to PostgreSQL:", err)
}
defer db.Close()

rdb, err := database.InitRedis(cfg.RedisURL)
if err != nil {
log.Fatal("Failed to connect to Redis:", err)
}
defer rdb.Close()

// Create Fiber app
app := fiber.New(fiber.Config{
ErrorHandler: func(c *fiber.Ctx, err error) error {
code := fiber.StatusInternalServerError
if e, ok := err.(*fiber.Error); ok {
code = e.Code
}
return c.Status(code).JSON(fiber.Map{
"error": err.Error(),
})
},
})

// Middleware
app.Use(logger.New())
app.Use(recover.New())
app.Use(cors.New())

// Setup routes
routes.Setup(app, db, rdb)

// Graceful shutdown
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)

go func() {
<-c
log.Println("Gracefully shutting down...")
app.Shutdown()
}()

log.Printf("Server starting on port %s", cfg.Port)
log.Fatal(app.Listen(":" + cfg.Port))
}
52 changes: 52 additions & 0 deletions go-fiber/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
version: '3.8'

services:
postgres:
image: postgres:15-alpine
environment:
POSTGRES_USER: productuser
POSTGRES_PASSWORD: productpass
POSTGRES_DB: productdb
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
- ./migrations:/docker-entrypoint-initdb.d
healthcheck:
test: ["CMD-SHELL", "pg_isready -U productuser -d productdb"]
interval: 10s
timeout: 5s
retries: 5

redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5

app:
build: .
ports:
- "8080:8080"
environment:
- PORT=8080
- DATABASE_URL=postgres://productuser:productpass@postgres:5432/productdb?sslmode=disable
- REDIS_URL=redis://redis:6379
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
volumes:
- .:/app
working_dir: /app

volumes:
postgres_data:
redis_data:
25 changes: 25 additions & 0 deletions go-fiber/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
module your-project

go 1.21

require (
github.com/gofiber/fiber/v2 v2.52.0
github.com/google/uuid v1.3.0
github.com/lib/pq v1.10.9
github.com/redis/go-redis/v9 v9.3.0
)

require (
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/klauspost/compress v1.17.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.51.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/sys v0.15.0 // indirect
)
26 changes: 26 additions & 0 deletions go-fiber/internal/config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package config

import (
"os"
)

type Config struct {
Port string
DatabaseURL string
RedisURL string
}

func Load() *Config {
return &Config{
Port: getEnv("PORT", "8080"),
DatabaseURL: getEnv("DATABASE_URL", "postgres://user:password@localhost/dbname?sslmode=disable"),
RedisURL: getEnv("REDIS_URL", "redis://localhost:6379"),
}
}

func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
63 changes: 63 additions & 0 deletions go-fiber/internal/database/postgres.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package database

import (
"database/sql"
"fmt"

_ "github.com/lib/pq"
)

func InitPostgres(databaseURL string) (*sql.DB, error) {
db, err := sql.Open("postgres", databaseURL)
if err != nil {
return nil, fmt.Errorf("failed to open database: %w", err)
}

if err := db.Ping(); err != nil {
return nil, fmt.Errorf("failed to ping database: %w", err)
}

// Run migrations
if err := runMigrations(db); err != nil {
return nil, fmt.Errorf("failed to run migrations: %w", err)
}

return db, nil
}

func runMigrations(db *sql.DB) error {
query := `
CREATE TABLE IF NOT EXISTS products (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
price DECIMAL(10,2) NOT NULL,
quantity INTEGER NOT NULL,
metadata JSONB,
related_ids JSONB,
categories JSONB,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);

CREATE TABLE IF NOT EXISTS product_ratings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
product_id UUID REFERENCES products(id) ON DELETE CASCADE,
score DECIMAL(2,1) NOT NULL CHECK (score >= 1 AND score <= 5),
created_at TIMESTAMP DEFAULT NOW()
);

CREATE TABLE IF NOT EXISTS product_tags (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
product_id UUID REFERENCES products(id) ON DELETE CASCADE,
tag VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE(product_id, tag)
);

CREATE INDEX IF NOT EXISTS idx_product_tags_tag ON product_tags(tag);
CREATE INDEX IF NOT EXISTS idx_product_ratings_product_id ON product_ratings(product_id);
`

_, err := db.Exec(query)
return err
}
24 changes: 24 additions & 0 deletions go-fiber/internal/database/redis.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package database

import (
"context"
"fmt"

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

func InitRedis(redisURL string) (*redis.Client, error) {
opt, err := redis.ParseURL(redisURL)
if err != nil {
return nil, fmt.Errorf("failed to parse redis URL: %w", err)
}

rdb := redis.NewClient(opt)

ctx := context.Background()
if err := rdb.Ping(ctx).Err(); err != nil {
return nil, fmt.Errorf("failed to ping redis: %w", err)
}

return rdb, nil
}
Loading