Skip to content

Commit 3ac883a

Browse files
authored
Merge pull request #2701 from norri/add-clean-code-recipe
Add clean code example
2 parents e580e67 + ba7a922 commit 3ac883a

28 files changed

+978
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Here you can find the most **delicious** recipes to cook delicious meals using o
2323
- [AWS SAM Container](./aws-sam-container/README.md) - Containerized serverless applications with AWS SAM.
2424
- [Bootstrap](./bootstrap/README.md) - Integrating Bootstrap.
2525
- [Clean Architecture](./clean-architecture/README.md) - Implementing clean architecture in Go.
26+
- [Clean Code](./clean-code/README.md) - Implementing clean code in Go.
2627
- [Cloud Run](./cloud-run/README.md) - Deploying to Google Cloud Run.
2728
- [Colly Gorm](./colly-gorm/README.md) - Web scraping with Colly and GORM.
2829
- [CSRF](./csrf/README.md) - Cross-Site Request Forgery (CSRF) protection.

clean-code/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
db_data

clean-code/Dockerfile-local

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
FROM golang:1.23
2+
RUN apt update && apt upgrade -y && apt install -y git
3+
4+
WORKDIR /go/src/app
5+
COPY app ./
6+
RUN go mod tidy && go mod verify
7+
8+
ENTRYPOINT [ "go", "run", "." ]

clean-code/README.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
---
2+
title: Clean Code
3+
keywords: [clean, code, fiber, postgres, go]
4+
description: Implementing clean code in Go.
5+
---
6+
7+
# Clean Code Example
8+
9+
[![Github](https://img.shields.io/static/v1?label=&message=Github&color=2ea44f&style=for-the-badge&logo=github)](https://github.com/gofiber/recipes/tree/master/clean-code) [![StackBlitz](https://img.shields.io/static/v1?label=&message=StackBlitz&color=2ea44f&style=for-the-badge&logo=StackBlitz)](https://stackblitz.com/github/gofiber/recipes/tree/master/clean-code)
10+
11+
This is an example of a RESTful API built using the Fiber framework (https://gofiber.io/) and PostgreSQL as the database.
12+
13+
## Description of Clean Code
14+
15+
Clean code is a philosophy and set of practices aimed at writing code that is easy to understand, maintain, and extend. Key principles of clean code include:
16+
17+
- **Readability**: Code should be easy to read and understand.
18+
- **Simplicity**: Avoid unnecessary complexity.
19+
- **Consistency**: Follow consistent coding standards and conventions.
20+
- **Modularity**: Break down code into small, reusable, and independent modules.
21+
- **Testability**: Write code that is easy to test.
22+
23+
This Fiber app is a good example of clean code because:
24+
25+
- **Modular Structure**: The code is organized into distinct modules, making it easy to navigate and understand.
26+
- **Clear Separation of Concerns**: Different parts of the application (e.g., routes, handlers, services) are clearly separated, making the codebase easier to maintain and extend.
27+
- **Error Handling**: Proper error handling is implemented to ensure the application behaves predictably.
28+
29+
## Start
30+
31+
1. Build and start the containers:
32+
```sh
33+
docker compose up --build
34+
```
35+
36+
1. The application should now be running and accessible at `http://localhost:3000`.
37+
38+
## Endpoints
39+
40+
- `GET /api/v1/books`: Retrieves a list of all books.
41+
```sh
42+
curl -X GET http://localhost:3000/api/v1/books
43+
```
44+
45+
- `POST /api/v1/books`: Adds a new book to the collection.
46+
```sh
47+
curl -X POST http://localhost:3000/api/v1/books \
48+
-H "Content-Type: application/json" \
49+
-d '{"title":"Title"}'
50+
```

clean-code/app/config.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package main
2+
3+
import (
4+
"log/slog"
5+
"os"
6+
)
7+
8+
// Configuration is used to store values from environment variables
9+
type Configuration struct {
10+
Port string
11+
DatabaseURL string
12+
}
13+
14+
// NewConfiguration reads environment variables and returns a new Configuration
15+
func NewConfiguration() *Configuration {
16+
dbURL := getEnvOrDefault("DATABASE_URL", "")
17+
if dbURL == "" {
18+
slog.Warn("DATABASE_URL is not set")
19+
}
20+
return &Configuration{
21+
Port: getEnvOrDefault("PORT", "3000"),
22+
DatabaseURL: dbURL,
23+
}
24+
}
25+
26+
func getEnvOrDefault(key, defaultValue string) string {
27+
if value, exists := os.LookupEnv(key); exists {
28+
return value
29+
}
30+
return defaultValue
31+
}

clean-code/app/config_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package main
2+
3+
import (
4+
"os"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func TestNewConfiguration(t *testing.T) {
11+
os.Setenv("PORT", "8080")
12+
os.Setenv("DATABASE_URL", "postgres://user:pass@localhost:5432/dbname")
13+
defer os.Unsetenv("PORT")
14+
defer os.Unsetenv("DATABASE_URL")
15+
16+
conf := NewConfiguration()
17+
18+
assert.Equal(t, "8080", conf.Port)
19+
assert.Equal(t, "postgres://user:pass@localhost:5432/dbname", conf.DatabaseURL)
20+
}
21+
22+
func TestNewConfiguration_Defaults(t *testing.T) {
23+
os.Unsetenv("PORT")
24+
os.Unsetenv("DATABASE_URL")
25+
26+
conf := NewConfiguration()
27+
28+
assert.Equal(t, "3000", conf.Port)
29+
assert.Equal(t, "", conf.DatabaseURL)
30+
}
31+
32+
func TestGetEnvOrDefault(t *testing.T) {
33+
os.Setenv("TEST_ENV", "value")
34+
defer os.Unsetenv("TEST_ENV")
35+
36+
value := getEnvOrDefault("TEST_ENV", "default")
37+
assert.Equal(t, "value", value)
38+
39+
value = getEnvOrDefault("NON_EXISTENT_ENV", "default")
40+
assert.Equal(t, "default", value)
41+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package datasources
2+
3+
import "app/datasources/database"
4+
5+
// DataSources is a struct that contains all the data sources
6+
// It is used to pass different data sources to the server and services
7+
type DataSources struct {
8+
DB database.Database
9+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package database
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"log/slog"
7+
"strings"
8+
)
9+
10+
// Book represents a book in the database
11+
type Book struct {
12+
ID int
13+
Title string
14+
}
15+
16+
// NewBook represents a new book to be created to the database
17+
type NewBook struct {
18+
Title string
19+
}
20+
21+
// Database defines the interface for interacting with the book database.
22+
// Using this interface allows changing the implementation without affecting the rest of the code.
23+
type Database interface {
24+
// LoadAllBooks retrieves all books from the database.
25+
LoadAllBooks(ctx context.Context) ([]Book, error)
26+
27+
// CreateBook adds a new book to the database.
28+
CreateBook(ctx context.Context, newBook NewBook) error
29+
30+
// CloseConnections closes all open connections to the database.
31+
CloseConnections()
32+
}
33+
34+
// NewDatabase creates a new Database instance
35+
func NewDatabase(ctx context.Context, databaseURL string) (Database, error) {
36+
if databaseURL == "" {
37+
slog.Info("Using in-memory database implementation")
38+
return newMemoryDB(), nil
39+
}
40+
41+
if strings.HasPrefix(databaseURL, "postgres://") {
42+
db, err := newPostgresDB(ctx, databaseURL)
43+
if err != nil {
44+
return nil, fmt.Errorf("failed to initialize PostgreSQL database connection: %w", err)
45+
}
46+
slog.Info("Using PostgreSQL database implementation")
47+
return db, nil
48+
}
49+
50+
return nil, fmt.Errorf("unsupported database URL scheme: %s", databaseURL)
51+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package database
2+
3+
import (
4+
"context"
5+
6+
"github.com/stretchr/testify/mock"
7+
)
8+
9+
type DatabaseMock struct {
10+
mock.Mock
11+
}
12+
13+
func (m *DatabaseMock) LoadAllBooks(ctx context.Context) ([]Book, error) {
14+
args := m.Called(ctx)
15+
if args.Get(0) == nil {
16+
return nil, args.Error(1)
17+
}
18+
return args.Get(0).([]Book), args.Error(1)
19+
}
20+
21+
func (m *DatabaseMock) CreateBook(ctx context.Context, newBook NewBook) error {
22+
args := m.Called(ctx, newBook)
23+
return args.Error(0)
24+
}
25+
26+
func (m *DatabaseMock) CloseConnections() {
27+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package database
2+
3+
import (
4+
"context"
5+
"reflect"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
func TestNewDatabase_MemoryDB(t *testing.T) {
12+
ctx := context.Background()
13+
db, err := NewDatabase(ctx, "")
14+
assert.Nil(t, err)
15+
assert.Equal(t, "*database.memoryDB", reflect.TypeOf(db).String())
16+
}
17+
18+
func TestNewDatabase_PostgresDB(t *testing.T) {
19+
ctx := context.Background()
20+
db, err := NewDatabase(ctx, "postgres://localhost:5432")
21+
assert.Nil(t, err)
22+
assert.Equal(t, "*database.postgresDB", reflect.TypeOf(db).String())
23+
}
24+
25+
func TestNewDatabase_InvalidDatabaseConfiguration(t *testing.T) {
26+
ctx := context.Background()
27+
_, err := NewDatabase(ctx, "invalid")
28+
assert.ErrorContains(t, err, "unsupported database")
29+
}
30+
31+
func assertBook(t *testing.T, book Book, expectedID int, expected NewBook) {
32+
assert.Equal(t, expectedID, book.ID)
33+
assert.Equal(t, expected.Title, book.Title)
34+
}

0 commit comments

Comments
 (0)