Skip to content

Commit 4a7ee56

Browse files
committed
add: containerize configure
1 parent adff643 commit 4a7ee56

File tree

28 files changed

+361
-140
lines changed

28 files changed

+361
-140
lines changed

.env.example

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
PORT=8000
2+
RATE_LIMIT=100
23
IDLE_TIMEOUT=5
4+
FAILED_ATTEMPS=5
35
ENABLE_PRINT_ROUTE=true
46

57
APP_ENV=local
@@ -10,13 +12,14 @@ LOG_LEVEL=debug
1012

1113
DB_HOST=localhost
1214
DB_PORT=4000
13-
DB_USER=root
15+
DB_USER=xxxx
1416
DB_PASSWORD=
15-
DB_NAME=test
17+
DB_NAME=xxxx
1618
DB_USE_SSL=false
1719
DB_AUTO_MIGRATE=true
1820

1921
REDIS_ADDR=localhost
2022
REDIS_PORT=6379
2123

22-
JWT_SECRET=c62028e29ccec50c690c3579ea5f87b4f5cfa92bd4cf039ae0b05975b8cbc1b4
24+
JWT_SECRET=xxxxxxxxxxx
25+
AES_KEY=xxxxxx

Dockerfile

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
FROM alpine:latest
2+
3+
WORKDIR /app
4+
5+
RUN apk --no-cache add ca-certificates
6+
7+
COPY bin/app .
8+
COPY .env .
9+
10+
EXPOSE 8000
11+
CMD ["./app"]

README.md

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
11
# Golang CRUD Service
22

3-
This is a sample CRUD service built with Golang, demonstrating a layered architecture that separates concerns into handler, service, repository, and model layers. The project uses **TiDB** as the relational database, **Redis** for caching and other in-memory operations, and **JWT** for authentication.
3+
This is a sample CRUD service built with **Golang**, demonstrating a layered architecture that separates concerns into handler, service, repository, and model layers. The project uses **TiDB** as the relational database, **Redis** for `caching` and `pub/sub` in-memory operations, and **JWT** for authentication.
44

55
## 🚀 Project Purpose
66

7-
Build a sample CRUD service API with Golang to showcase clean code structure and common best practices in web API development.
7+
Build a sample CRUD service API with Golang to showcase code structure and common best practices in web API development.
88

99
## 🛠️ Tech Stack
1010

1111
- **[Golang](https://golang.org)** — Core programming language
1212
- **[Fiber](https://gofiber.io)** — Fast HTTP router
1313
- **[GORM](https://gorm.io)** — ORM for database operations
1414
- **[TiDB](https://www.pingcap.com)** — Distributed SQL database
15-
- **[Redis](https://redis.io)** — In-memory data store
1615
- **[Validator](https://github.com/go-playground/validator)** — Input validation
1716
- **[Copier](https://github.com/jinzhu/copier)** — Object copying for DTOs
17+
- **[Redis](https://redis.io)** — Distributed In-memory data caching
18+
- **[Redis Stream](https://redis.io/docs/latest/develop/data-types/streams)** — Background task queue using consumer group
1819
- **[JWT](https://github.com/golang-jwt/jwt)** — JSON Web Token implementation for authentication
1920

2021
## 📁 Project Structure
@@ -24,19 +25,22 @@ Build a sample CRUD service API with Golang to showcase clean code structure and
2425
│ └── app/
2526
│ └── main.go # Application entry point
2627
├── pkg/ # Shared package configure
27-
── crypto/
28-
── aes.go # Advanced Encryption Standard encryption
29-
└── bcrypt.go # Bcrypt hashing
30-
── redis/
31-
└── redis.go # Redis configuration
28+
── crypto/
29+
── aes.go # Advanced Encryption Standard
30+
└── bcrypt.go # Bcrypt hashing
31+
── redis/
32+
└── redis.go # Redis configuration
3233
│ └── db/
3334
│ └── db.go # DB configuration
3435
├── api/
36+
│ ├── auth/
37+
│ │ ├── routes.go # User authenticate-related endpoints
38+
│ │ └── handler.go # Logic for handling auth API requests
3539
│ └── student/
36-
│ ├── route.go # Student-related endpoints
40+
│ ├── routes.go # Student-related endpoints
3741
│ └── handler.go # Logic for handling student API requests
3842
├── internal/
39-
│ ├── middleware/ # HTTP middleware (e.g., auth, logging)
43+
│ ├── middleware/ # HTTP middleware (e.g., auth, rate limit, logging)
4044
│ ├── repository/ # Data access layer (calls GORM & queries DB)
4145
│ ├── service/ # Business logic, called from handler
4246
│ ├── model/ # Database models/entities
@@ -46,9 +50,11 @@ Build a sample CRUD service API with Golang to showcase clean code structure and
4650
## 📌 Key Concepts
4751

4852
- **Layered Architecture**: Divides the project into clear layers for maintainability and scalability.
53+
- **Goroutines**: Background task workers use `goroutines` to run concurrently and process `redis stream` jobs efficiently.
54+
- **Rate Limit**: Failed attempts and request limited using `redis` to prevent abuse `429 Too Many Requests`.
4955
- **DTO Pattern**: Uses `copier` to map between internal models and request/response structures.
5056
- **Validation**: Ensures request payloads are validated with `go-playground/validator`.
51-
- **ORM**: Leverages GORM to interact with TiDB in a concise and type-safe way.
57+
- **ORM**: Leverages `GORM` to interact with `TiDB` in a concise and type-safe way.
5258

5359
## 🧪 Running the Project
5460

@@ -63,10 +69,19 @@ Build a sample CRUD service API with Golang to showcase clean code structure and
6369
go run cmd/app/main.go
6470
```
6571

72+
3. **Build the App**
73+
```bash
74+
GOOS=linux GOARCH=amd64 go build -o bin/app ./cmd/app
75+
```
76+
77+
4. **Containerize the App**
78+
```bash
79+
docker compose up -d
80+
```
81+
6682
## 🔧 Future Enhancements
6783

6884
- Add unit tests and integration tests
69-
- Dockerize the application
7085
- Add Swagger/OpenAPI documentation
7186

7287
## 📄 License

api/auth/handler.go

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package auth
22

33
import (
4-
"github.com/gofiber/fiber/v2"
5-
"github.com/jinzhu/copier"
64
"gorm.io/gorm"
75
"learn-fiber/api/response"
86
"learn-fiber/internal/constant"
@@ -11,6 +9,10 @@ import (
119
"learn-fiber/internal/service"
1210
"learn-fiber/internal/util/common"
1311
"learn-fiber/internal/util/validator"
12+
"net/http"
13+
14+
"github.com/gofiber/fiber/v2"
15+
"github.com/jinzhu/copier"
1416
)
1517

1618
type Handler struct {
@@ -27,9 +29,9 @@ func (h *Handler) Login(c *fiber.Ctx) error {
2729
if hasError, err := validator.V.Valid(req); hasError {
2830
return ierror.NewValidationError(err)
2931
}
30-
result, err := h.svc.Login(req)
32+
result, err := h.svc.Login(c.IP(), req)
3133
if err != nil {
32-
return ierror.NewClientError(200, ierror.ErrCodeAuthenticationError, err.Error())
34+
return ierror.NewClientError(http.StatusOK, ierror.ErrCodeAuthenticationError, err.Error())
3335
}
3436
var res TokenResponse
3537
if err = copier.Copy(&res, &result); err != nil {
@@ -43,7 +45,7 @@ func (h *Handler) Logout(c *fiber.Ctx) error {
4345
if err != nil {
4446
return ierror.NewAuthenticationError(ierror.ErrCodeAuthenticationError, err.Error())
4547
}
46-
if err := h.svc.Logout(token); err != nil {
48+
if err = h.svc.Logout(token); err != nil {
4749
return ierror.NewServerError(ierror.ErrCodeTokenError, err.Error())
4850
}
4951
return response.SendSuccessResponse(c, constant.Success)
@@ -54,22 +56,21 @@ func (h *Handler) RenewToken(c *fiber.Ctx) error {
5456
if hasError, err := validator.V.Valid(req); hasError {
5557
return ierror.NewValidationError(err)
5658
}
57-
result, err := h.svc.RenewToken(req)
59+
result, err := h.svc.RenewToken(c.IP(), req)
5860
if err != nil {
59-
return ierror.NewClientError(200, ierror.ErrCodeAuthenticationError, err.Error())
61+
return ierror.NewClientError(http.StatusOK, ierror.ErrCodeDataNotFound, err.Error())
6062
}
6163
var res TokenResponse
6264
if err = copier.Copy(&res, &result); err != nil {
6365
return ierror.NewServerError(ierror.ErrCodeDtoError, err.Error())
6466
}
6567
return response.SendSuccessResponse(c, res)
66-
6768
}
6869

6970
func (h *Handler) Info(c *fiber.Ctx) error {
7071
result, err := h.svc.Info(common.GetUser(c))
7172
if err != nil {
72-
return ierror.NewClientError(200, ierror.ErrCodeAuthenticationError, err.Error())
73+
return ierror.NewClientError(http.StatusOK, ierror.ErrCodeAuthenticationError, err.Error())
7374
}
7475
var res UserResponse
7576
if err = copier.Copy(&res, &result); err != nil {
@@ -84,7 +85,7 @@ func (h *Handler) ChangePassword(c *fiber.Ctx) error {
8485
return ierror.NewValidationError(err)
8586
}
8687
if err := h.svc.ChangePassword(common.GetUser(c), req); err != nil {
87-
return ierror.NewClientError(200, ierror.ErrCodeValidationError, err.Error())
88+
return ierror.NewClientError(http.StatusOK, ierror.ErrCodeValidationError, err.Error())
8889
}
8990
return response.SendSuccessResponse(c, constant.Success)
9091
}

api/auth/response.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@ type TokenResponse struct {
99
type UserResponse struct {
1010
Id uint `json:"id"`
1111
Username string `json:"username"`
12+
Email string `json:"email"`
1213
Role string `json:"role"`
1314
}

api/department/handler.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"learn-fiber/internal/service"
1010
"learn-fiber/internal/util/common"
1111
"learn-fiber/internal/util/validator"
12+
"net/http"
1213

1314
"github.com/gofiber/fiber/v2"
1415
"github.com/jinzhu/copier"
@@ -80,7 +81,7 @@ func (h *Handler) Update(c *fiber.Ctx) error {
8081
result, err := h.svc.UpdateDepartment(req)
8182
if err != nil {
8283
if errors.Is(err, gorm.ErrRecordNotFound) {
83-
return ierror.NewClientError(200, ierror.ErrCodeDataNotFound, err.Error())
84+
return ierror.NewClientError(http.StatusOK, ierror.ErrCodeDataNotFound, err.Error())
8485
}
8586
return ierror.NewServerError(ierror.ErrCodeDatabaseError, err.Error())
8687
}

api/response/error.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ type ErrorObject struct {
88
Code string `json:"code"`
99
Message string `json:"message"`
1010
ValidationErrors []ValidationError `json:"validationErrors,omitempty"`
11-
TraceID string `json:"traceID"`
11+
TraceID string `json:"traceID,omitempty"`
1212
}
1313

1414
type ValidationError struct {

api/student/handler.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"learn-fiber/internal/service"
1010
"learn-fiber/internal/util/common"
1111
"learn-fiber/internal/util/validator"
12+
"net/http"
1213

1314
"github.com/gofiber/fiber/v2"
1415
"github.com/jinzhu/copier"
@@ -45,7 +46,7 @@ func (h *Handler) GetById(c *fiber.Ctx) error {
4546
result, err := h.svc.GetStudent(id)
4647
if err != nil {
4748
if errors.Is(err, gorm.ErrRecordNotFound) {
48-
return ierror.NewClientError(200, ierror.ErrCodeDataNotFound, err.Error())
49+
return ierror.NewClientError(http.StatusOK, ierror.ErrCodeDataNotFound, err.Error())
4950
}
5051
return ierror.NewServerError(ierror.ErrCodeDatabaseError, err.Error())
5152
}
@@ -80,7 +81,7 @@ func (h *Handler) Update(c *fiber.Ctx) error {
8081
result, err := h.svc.UpdateStudent(req)
8182
if err != nil {
8283
if errors.Is(err, gorm.ErrRecordNotFound) {
83-
return ierror.NewClientError(200, ierror.ErrCodeDataNotFound, err.Error())
84+
return ierror.NewClientError(http.StatusOK, ierror.ErrCodeDataNotFound, err.Error())
8485
}
8586
return ierror.NewServerError(ierror.ErrCodeDatabaseError, err.Error())
8687
}

config/config.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ type (
1313
Config struct {
1414
Host string `mapstructure:"host"`
1515
Port string `mapstructure:"port"`
16+
RateLimit int `mapstructure:"rate_limit"`
1617
IdleTimeout int `mapstructure:"idle_timeout"`
18+
FailedAttempts int `mapstructure:"failed_attempts"`
1719
EnablePrintRoute bool `mapstructure:"enable_print_route"`
1820

1921
App `mapstructure:",squash"`
@@ -92,7 +94,9 @@ func Init() error {
9294
// 3. replace match any of the existing env vars used in flags
9395
pflag.String("host", "", "server host")
9496
pflag.Int("port", 8000, "server port")
97+
pflag.Int("rate_limit", 100, "rate limit")
9598
pflag.Int("idle_timeout", 5, "server idle timeout")
99+
pflag.Int("failed_attempts", 5, "login failed attempts")
96100
pflag.Bool("enable_print_route", true, "server enable print route")
97101

98102
pflag.String("app_env", "local", "app env")
@@ -110,7 +114,7 @@ func Init() error {
110114
pflag.Bool("db_auto_migrate", false, "database auto migrate")
111115

112116
pflag.String("redis_addr", "", "redis address")
113-
pflag.String("redis_port", "15951", "redis port")
117+
pflag.String("redis_port", "6379", "redis port")
114118
pflag.String("redis_pwd", "", "redis password")
115119

116120
pflag.String("tab_enable_text", "", "tablenames before split")
@@ -127,7 +131,9 @@ func Init() error {
127131
Cfg = &Config{
128132
Host: viper.GetString("host"),
129133
Port: viper.GetString("port"),
134+
RateLimit: viper.GetInt("rate_limit"),
130135
IdleTimeout: viper.GetInt("idle_timeout"),
136+
FailedAttempts: viper.GetInt("failed_attempts"),
131137
EnablePrintRoute: viper.GetBool("enable_print_route"),
132138
App: App{
133139
Env: viper.GetString("app_env"),

docker-compose.yaml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
version: '3.8'
2+
3+
services:
4+
tidb:
5+
image: pingcap/tidb:latest
6+
ports:
7+
- "4000:4000"
8+
networks:
9+
- sample-net
10+
redis:
11+
image: redis:alpine
12+
ports:
13+
- "6379:6379"
14+
networks:
15+
- sample-net
16+
app:
17+
container_name: sample-crud
18+
image: sample-crud
19+
build:
20+
context: .
21+
dockerfile: Dockerfile
22+
ports:
23+
- "8000:8000"
24+
depends_on:
25+
- redis
26+
- tidb
27+
networks:
28+
- sample-net
29+
environment:
30+
- TZ=Asia/Phnom_Penh
31+
- REDIS_ADDR=redis
32+
- DB_HOST=tidb
33+
networks:
34+
sample-net:

0 commit comments

Comments
 (0)