Skip to content

Commit

Permalink
chore: added fiber examples
Browse files Browse the repository at this point in the history
  • Loading branch information
caiocampos committed Dec 19, 2024
1 parent 1944642 commit 62aa9cb
Show file tree
Hide file tree
Showing 31 changed files with 1,399 additions and 0 deletions.
1 change: 1 addition & 0 deletions example/clean-code/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
db_data
8 changes: 8 additions & 0 deletions example/clean-code/Dockerfile-local
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
FROM golang:1.23
RUN apt update && apt upgrade -y && apt install -y git

WORKDIR /go/src/app
COPY app ./
RUN go mod tidy && go mod verify

ENTRYPOINT [ "go", "run", "." ]
50 changes: 50 additions & 0 deletions example/clean-code/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
title: Clean Code
keywords: [clean, code, fiber, postgres, go]
description: Implementing clean code in Go.
---

# Clean Code Example

[![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)

This is an example of a RESTful API built using the Fiber framework (https://gofiber.io/) and PostgreSQL as the database.

## Description of Clean Code

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:

- **Readability**: Code should be easy to read and understand.
- **Simplicity**: Avoid unnecessary complexity.
- **Consistency**: Follow consistent coding standards and conventions.
- **Modularity**: Break down code into small, reusable, and independent modules.
- **Testability**: Write code that is easy to test.

This Fiber app is a good example of clean code because:

- **Modular Structure**: The code is organized into distinct modules, making it easy to navigate and understand.
- **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.
- **Error Handling**: Proper error handling is implemented to ensure the application behaves predictably.

## Start

1. Build and start the containers:
```sh
docker compose up --build
```

1. The application should now be running and accessible at `http://localhost:3000`.

## Endpoints

- `GET /api/v1/books`: Retrieves a list of all books.
```sh
curl -X GET http://localhost:3000/api/v1/books
```

- `POST /api/v1/books`: Adds a new book to the collection.
```sh
curl -X POST http://localhost:3000/api/v1/books \
-H "Content-Type: application/json" \
-d '{"title":"Title"}'
```
31 changes: 31 additions & 0 deletions example/clean-code/app/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package main

import (
"log/slog"
"os"
)

// Configuration is used to store values from environment variables
type Configuration struct {
Port string
DatabaseURL string
}

// NewConfiguration reads environment variables and returns a new Configuration
func NewConfiguration() *Configuration {
dbURL := getEnvOrDefault("DATABASE_URL", "")
if dbURL == "" {
slog.Warn("DATABASE_URL is not set")
}
return &Configuration{
Port: getEnvOrDefault("PORT", "3000"),
DatabaseURL: dbURL,
}
}

func getEnvOrDefault(key, defaultValue string) string {
if value, exists := os.LookupEnv(key); exists {
return value
}
return defaultValue
}
41 changes: 41 additions & 0 deletions example/clean-code/app/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package main

import (
"os"
"testing"

"github.com/stretchr/testify/assert"
)

func TestNewConfiguration(t *testing.T) {
os.Setenv("PORT", "8080")
os.Setenv("DATABASE_URL", "postgres://user:pass@localhost:5432/dbname")
defer os.Unsetenv("PORT")
defer os.Unsetenv("DATABASE_URL")

conf := NewConfiguration()

assert.Equal(t, "8080", conf.Port)
assert.Equal(t, "postgres://user:pass@localhost:5432/dbname", conf.DatabaseURL)
}

func TestNewConfiguration_Defaults(t *testing.T) {
os.Unsetenv("PORT")
os.Unsetenv("DATABASE_URL")

conf := NewConfiguration()

assert.Equal(t, "3000", conf.Port)
assert.Equal(t, "", conf.DatabaseURL)
}

func TestGetEnvOrDefault(t *testing.T) {
os.Setenv("TEST_ENV", "value")
defer os.Unsetenv("TEST_ENV")

value := getEnvOrDefault("TEST_ENV", "default")
assert.Equal(t, "value", value)

value = getEnvOrDefault("NON_EXISTENT_ENV", "default")
assert.Equal(t, "default", value)
}
9 changes: 9 additions & 0 deletions example/clean-code/app/datasources/data_sources.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package datasources

import "app/datasources/database"

// DataSources is a struct that contains all the data sources
// It is used to pass different data sources to the server and services
type DataSources struct {
DB database.Database
}
51 changes: 51 additions & 0 deletions example/clean-code/app/datasources/database/db.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package database

import (
"context"
"fmt"
"log/slog"
"strings"
)

// Book represents a book in the database
type Book struct {
ID int
Title string
}

// NewBook represents a new book to be created to the database
type NewBook struct {
Title string
}

// Database defines the interface for interacting with the book database.
// Using this interface allows changing the implementation without affecting the rest of the code.
type Database interface {
// LoadAllBooks retrieves all books from the database.
LoadAllBooks(ctx context.Context) ([]Book, error)

// CreateBook adds a new book to the database.
CreateBook(ctx context.Context, newBook NewBook) error

// CloseConnections closes all open connections to the database.
CloseConnections()
}

// NewDatabase creates a new Database instance
func NewDatabase(ctx context.Context, databaseURL string) (Database, error) {
if databaseURL == "" {
slog.Info("Using in-memory database implementation")
return newMemoryDB(), nil
}

if strings.HasPrefix(databaseURL, "postgres://") {
db, err := newPostgresDB(ctx, databaseURL)
if err != nil {
return nil, fmt.Errorf("failed to initialize PostgreSQL database connection: %w", err)
}
slog.Info("Using PostgreSQL database implementation")
return db, nil
}

return nil, fmt.Errorf("unsupported database URL scheme: %s", databaseURL)
}
27 changes: 27 additions & 0 deletions example/clean-code/app/datasources/database/db_mock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package database

import (
"context"

"github.com/stretchr/testify/mock"
)

type DatabaseMock struct {
mock.Mock
}

func (m *DatabaseMock) LoadAllBooks(ctx context.Context) ([]Book, error) {
args := m.Called(ctx)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).([]Book), args.Error(1)
}

func (m *DatabaseMock) CreateBook(ctx context.Context, newBook NewBook) error {
args := m.Called(ctx, newBook)
return args.Error(0)
}

func (m *DatabaseMock) CloseConnections() {
}
34 changes: 34 additions & 0 deletions example/clean-code/app/datasources/database/db_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package database

import (
"context"
"reflect"
"testing"

"github.com/stretchr/testify/assert"
)

func TestNewDatabase_MemoryDB(t *testing.T) {
ctx := context.Background()
db, err := NewDatabase(ctx, "")
assert.Nil(t, err)
assert.Equal(t, "*database.memoryDB", reflect.TypeOf(db).String())
}

func TestNewDatabase_PostgresDB(t *testing.T) {
ctx := context.Background()
db, err := NewDatabase(ctx, "postgres://localhost:5432")
assert.Nil(t, err)
assert.Equal(t, "*database.postgresDB", reflect.TypeOf(db).String())
}

func TestNewDatabase_InvalidDatabaseConfiguration(t *testing.T) {
ctx := context.Background()
_, err := NewDatabase(ctx, "invalid")
assert.ErrorContains(t, err, "unsupported database")
}

func assertBook(t *testing.T, book Book, expectedID int, expected NewBook) {
assert.Equal(t, expectedID, book.ID)
assert.Equal(t, expected.Title, book.Title)
}
32 changes: 32 additions & 0 deletions example/clean-code/app/datasources/database/memory_db.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package database

import "context"

// This is just an example and not for production use
func newMemoryDB() Database {
return &memoryDB{
records: make([]Book, 0, 10),
idCounter: 0,
}
}

type memoryDB struct {
records []Book
idCounter int
}

func (db *memoryDB) LoadAllBooks(_ context.Context) ([]Book, error) {
return db.records, nil
}

func (db *memoryDB) CreateBook(_ context.Context, newBook NewBook) error {
db.records = append(db.records, Book{
ID: db.idCounter,
Title: newBook.Title,
})
db.idCounter++
return nil
}

func (db *memoryDB) CloseConnections() {
}
44 changes: 44 additions & 0 deletions example/clean-code/app/datasources/database/memory_db_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package database

import (
"context"
"testing"

"github.com/stretchr/testify/assert"
)

func TestMemoryDB_LoadBooks(t *testing.T) {
db := newMemoryDB()
books, err := db.LoadAllBooks(context.Background())
assert.Nil(t, err)
assert.Equal(t, 0, len(books))
}

func TestMemoryDB_SaveBook(t *testing.T) {
db := newMemoryDB()
newBook := NewBook{Title: "Title"}
err := db.CreateBook(context.Background(), newBook)
assert.Nil(t, err)

books, err := db.LoadAllBooks(context.Background())
assert.Nil(t, err)
assert.Equal(t, 1, len(books))
assertBook(t, books[0], 0, newBook)
}

func TestMemoryDB_SaveBookMultiple(t *testing.T) {
db := newMemoryDB()
newBook1 := NewBook{Title: "Title1"}
err := db.CreateBook(context.Background(), newBook1)
assert.Nil(t, err)

newBook2 := NewBook{Title: "Title2"}
err = db.CreateBook(context.Background(), newBook2)
assert.Nil(t, err)

books, err := db.LoadAllBooks(context.Background())
assert.Nil(t, err)
assert.Equal(t, 2, len(books))
assertBook(t, books[0], 0, newBook1)
assertBook(t, books[1], 1, newBook2)
}
Loading

0 comments on commit 62aa9cb

Please sign in to comment.