This application developed using gogen framework code generator Gogen Framework
We applied many design concept in this project like
- clean architecture (use case, input port, interactor, and output port)
- dependency injection
- non-anemic domain model
- domain driven design (entity, value object, service, repository)
- single responsibility principle
- open closed principle
- interface segregation principle
It also has a features like
- use decoupling 3 layer architecture
- integrating log each layer
- config
- error code collection
- modifiable responses code
- very clear separation of concern
- fixed, consistent but still flexible code structure
For more detail about the feature you can read here
It has one entity domain_item/model/entity/item.go
type Item struct {
ID vo.ItemID `json:"id" bson:"_id"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
Name string `json:"name"`
Rating vo.Rating `json:"rating" `
Category vo.Category `json:"category"`
ImageURL vo.StringURL `json:"image"`
Reputation vo.Reputation `json:"reputation"`
ReputationBadge string `json:"reputation_badge"`
Price int `json:"price"`
Availability int `json:"availability"`
}
A six use cases domain_item/usecase/
1. getallitem --> Get All Item with filter
2. getoneitem --> Get Only One Item by ID
3. runitemcreate --> Create an Item
4. runitemdelete --> Delete an Item by ID
5. runitempurchase --> Reduce the Availability of Item
6. runitemupdate --> Update an Item
Each use case, published via REST API using gin-gonic domain_item/controller/restapi/router.go
runitemcreate --> POST /api/v1/items
getallitem --> GET /api/v1/items
getoneitem --> GET /api/v1/items/:item_id
runitemupdate --> PUT /api/v1/items/:item_id
runitemdelete --> DELETE /api/v1/items/:item_id
runitempurchase --> POST /api/v1/items/:item_id/purchase
Project Structure
domain_item
├── controller
│ ├── restapi
│ └── restapi2
├── gateway
│ ├── shared
│ ├── withmongodb
│ ├── withmysqldb
│ └── withsqlitedb
├── model
│ ├── entity
│ ├── errorenum
│ ├── repository
│ ├── service
│ └── vo
└── usecase
├── getallitem
├── getoneitem
├── runitemcreate
├── runitemdelete
├── runitempurchase
└── runitemupdate
In this project demonstration, we have
- 2 alternative controller (gin, echo)
- 3 alternative gateway (sqlitem, mysql, mongodb)
you can choose to run this application with 2 alternative web framework.
- Gin (
domain_item/controller/restapi
) - Echo (
domain_item/controller/restapi
)
See application/app_appitem.go
primaryDriver := restapi.NewController(appData, log, cfg)
//primaryDriver := restapi2.NewController(appData, log, cfg)
By default, we use Gin web framework
you can decide to run this application with 3 alternative database
- SQLite using Gorm (
domain_item/gateway/withmysqldb
) - MySQL using Gorm (
domain_item/gateway/withsqlitedb
) - Native MongoDB (
domain_item/gateway/withmongodb
)
See application/app_appitem.go
datasource := withsqlitedb.NewGateway(log, appData, cfg)
//datasource := withmysqldb.NewGateway(log, appData, cfg)
//datasource := withmongodb.NewGateway(log, appData, cfg)
By default, it is running with SQLite db
Disclaimers!
in real life, it is very rare to have 3 alternative of database and/or 2 alternative web framework in one application . This is just for demonstration purposed
After git clone the code, open a terminal (we named it a 'first terminal'), then download the dependency by call this command
$ go mod tidy
Run it by
$ go run main.go appitem
then you will see the application is running
➜ theitem go run main.go appitem
Version 0.0.1
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] GET /ping --> theitem/domain_item/controller/restapi.NewController.func1 (3 handlers)
[GIN-debug] GET /web/*filepath --> github.com/gin-gonic/gin.(*RouterGroup).createStaticHandler.func1 (3 handlers)
[GIN-debug] HEAD /web/*filepath --> github.com/gin-gonic/gin.(*RouterGroup).createStaticHandler.func1 (3 handlers)
[GIN-debug] POST /api/v1/items --> theitem/domain_item/controller/restapi.(*controller).runItemCreateHandler.func1 (6 handlers)
[GIN-debug] GET /api/v1/items --> theitem/domain_item/controller/restapi.(*controller).getAllItemHandler.func1 (6 handlers)
[GIN-debug] GET /api/v1/items/:item_id --> theitem/domain_item/controller/restapi.(*controller).getOneItemHandler.func1 (6 handlers)
[GIN-debug] PUT /api/v1/items/:item_id --> theitem/domain_item/controller/restapi.(*controller).runItemUpdateHandler.func1 (6 handlers)
[GIN-debug] DELETE /api/v1/items/:item_id --> theitem/domain_item/controller/restapi.(*controller).runItemDeleteHandler.func1 (6 handlers)
[GIN-debug] POST /api/v1/items/:item_id/purchase --> theitem/domain_item/controller/restapi.(*controller).runItemPurchaseHandler.func1 (6 handlers)
INFO 0000000000000000 server is running at :8080 restapi.(*gracefullyShutdown).Start:40
Now you can use postman or curl for accessing the API. But it is better to use the UI. Keep reading, because we also provide the UI.
Currently, the API is running on port 8080. You may change the port through config.json
.
When running with SQLite db, we only use the db_name
field in config.json
and ignore the other database fields.
{
"database": {
"username": "root",
"password": "12345",
"host": "localhost",
"port": "27017",
"db_name": "itemdb"
},
"servers": {
"appItem": {
"address": ":8080"
}
}
}
This application also has a simple user interface (UI) for testing purpose.
The UI use all the capability of REST API.
It built using vue js stack under web/
directory.
To follow the further instruction of how to installing UI, make sure you already install nodejs in your system.
In order to run the UI, open new 'second terminal', change the directory
$ cd web
then install all the dependencies by running this command
$ npm install
While the backend apps is still running, run this command
$ npm run dev
Then you will see this output
➜ web npm run dev
> [email protected] dev
> vite
VITE v3.2.5 ready in 222 ms
➜ Local: http://localhost:5173/web/
➜ Network: use --host to expose
Open your browser then access http://localhost:5173/web/
You can run the frontend without running it separately from backend. In this case the backend will support the frontend as a webserver. All you need to do is build the web app into distribution package. Stop the frontend application in 'second terminal' (if frontend is still running), then run this command
$ npm run build
The command will create a bundled web application in folder web/dist/
.
Back to 'first terminal', stop the backend by ctrl+c
, then re-run it again
$ go run main.go appitem
Now, open your browser then access http://localhost:8080/web/
.
Notice that we are only running the backend apps without running the frontend apps.
Open file application/app_appitem.go
then change the code, from this
datasource := withsqlitedb.NewGateway(log, appData, cfg)
//datasource := withmysqldb.NewGateway(log, appData, cfg)
//datasource := withmongodb.NewGateway(log, appData, cfg)
Into this
//datasource := withsqlitedb.NewGateway(log, appData, cfg)
datasource := withmysqldb.NewGateway(log, appData, cfg)
//datasource := withmongodb.NewGateway(log, appData, cfg)
Open config.json
{
"database": {
"username": "root",
"password": "12345",
"host": "localhost",
"port": "3306",
"db_name": "itemdb"
},
"servers": {
"appItem": {
"address": ":8080"
}
}
}
adjust the config as necessary (for example : username and password)
Actually the process is same as using MySQL, we only switch the code in application/app_appitem.go
into this
//datasource := withsqlitedb.NewGateway(log, appData, cfg)
//datasource := withmysqldb.NewGateway(log, appData, cfg)
datasource := withmongodb.NewGateway(log, appData, cfg)
And then adjust the config.json
as necessary (please keep in mind by default, mongodb use port 27017)
Before running with docker, first decide whether you want to run it by SQLite, MySQL or MongoDB
by switching the implementation in application/app_appitem.go
.
By default (NOT using docker), this application use config.json
.
In docker version, it uses different config which is in config.prod.json
.
You can change which config to use in docker-compose.yml
file.
Currently, docker-compose.yml
file specify 2 database image (mongodb and mysql) and 1 application image (myapp).
Since SQLite is a embedded database, we don't need to use any docker image. By default, it will just run simply by calling
$ docker compose up
You need to enable this part on docker-compose.yml
mysqldb:
image: mysql
restart: always
environment:
- MYSQL_ROOT_PASSWORD=12345
- MYSQL_DATABASE=itemdb
ports:
- "3306:3306"
The config.prod.json
for MySQL (pay attention to the host and port)
{
"database": {
"username": "root",
"password": "12345",
"host": "mysqldb",
"port": "3306",
"db_name": "itemdb"
},
"servers": {
"appItem": {
"address": ":8080"
}
}
}
You need to enable this part on docker-compose.yml
mongodb:
image: mongo
ports:
- "27017:27017"
environment:
- MONGO_INITDB_ROOT_USERNAME=root
- MONGO_INITDB_ROOT_PASSWORD=12345
Adjust the config.prod.json
(change the host and port)
{
"database": {
"username": "root",
"password": "12345",
"host": "mongodb",
"port": "27017",
"db_name": "itemdb"
},
"servers": {
"appItem": {
"address": ":8080"
}
}
}
Run the docker compose (you may add -d
for running it in background)
$ docker compose up
Open browser then access
http://localhost:8080/web/
POST /api/v1/items
REQUEST
{
"name": "the first item",
"rating": 2,
"category": "cartoon",
"image": "http://image.aa",
"reputation": 34,
"price": 5000,
"availability": 10
}
RESPONSE OK
{
"success": true,
"errorCode": "",
"errorMessage": "",
"data": {},
"traceId": "KR9HW32UT28N0VKW"
}
RESPONSE FAIL
{
"success": false,
"errorCode": "ER0006",
"errorMessage": "name length must greater than 10",
"data": null,
"traceId": "HO6ONN4SICA1UHTY"
}
RESPONSE FAIL
{
"success": false,
"errorCode": "ER0009",
"errorMessage": "item with name 'the first item' already exist",
"data": null,
"traceId": "3Z1XZGHL1X14YYKD"
}
RESPONSE FAIL
{
"success": false,
"errorCode": "ER0005",
"errorMessage": "word 'sex' is not allowed",
"data": null,
"traceId": "FKSAX0YK1MO6XS82"
}
RESPONSE FAIL
{
"success": false,
"errorCode": "ER0004",
"errorMessage": "invalid rating value. must be integer between 0..5",
"data": null,
"traceId": "NT325V3DN8MLDC0A"
}
RESPONSE FAIL
{
"success": false,
"errorCode": "ER0003",
"errorMessage": "invalid category. must be one of [photo sketch cartoon animation]",
"data": null,
"traceId": "NJXGNC72X60GLUBW"
}
RESPONSE FAIL
{
"success": false,
"errorCode": "ER0002",
"errorMessage": "invalid url for 'image'",
"data": null,
"traceId": "91T18FT3SFFFLNVA"
}
RESPONSE FAIL
{
"success": false,
"errorCode": "ER0001",
"errorMessage": "out of range reputation. must between 0 to 1000",
"data": null,
"traceId": "DS4YVNSPGCGHPRMF"
}
RESPONSE FAIL
{
"success": false,
"errorCode": "ER0011",
"errorMessage": "price must greater or equal zero",
"data": null,
"traceId": "FXA7LLI93HOX9HFF"
}
GET /api/v1/items
page=1&
size=2&
rating=3&
reputation_badge=yellow&
availability_more=0&
availability_less=100&
category=photo
RESPONSE OK
{
"success": true,
"errorCode": "",
"errorMessage": "",
"data": {
"count": 2,
"items": [
{
"id": "0caf9621-aab4-4fc8-a133-47fc98ec36cf",
"created": "2023-02-12T09:21:46.947388+07:00",
"updated": "2023-02-12T09:21:46.947388+07:00",
"name": "the first item",
"rating": 2,
"category": "animation",
"image": "http://image.aa",
"reputation": 34,
"reputation_badge": "red",
"price": 5000,
"availability": 10
},
{
"id": "de5aafdc-3361-4e69-83ea-5529b21f255e",
"created": "2023-02-12T09:22:25.311257+07:00",
"updated": "2023-02-12T09:22:25.311257+07:00",
"name": "the second item",
"rating": 2,
"category": "cartoon",
"image": "http://image.aa",
"reputation": 34,
"reputation_badge": "red",
"price": 5000,
"availability": 10
}
]
},
"traceId": "K2VRLG7OC6AP5IGN"
}
GET /api/v1/items/0caf9621-aab4-4fc8-a133-47fc98ec36cf
RESPONSE
{
"success": true,
"errorCode": "",
"errorMessage": "",
"data": {
"item": {
"id": "0caf9621-aab4-4fc8-a133-47fc98ec36cf",
"created": "2023-02-12T09:21:46.947388+07:00",
"updated": "2023-02-12T09:21:46.947388+07:00",
"name": "the first item",
"rating": 2,
"category": "animation",
"image": "http://image.aa",
"reputation": 34,
"reputation_badge": "red",
"price": 5000,
"availability": 10
}
},
"traceId": "1W9DAWJPNSFCWY38"
}
RESPONSE FAIL
{
"success": false,
"errorCode": "ER0007",
"errorMessage": "unavailable item with id 'abcd9621-aab4-4fc8-a133-47fc98ec61de'",
"data": null,
"traceId": "EH863OJWJUGB2ERC"
}
PUT /api/v1/items/0caf9621-aab4-4fc8-a133-47fc98ec36cf
REQUEST
{
"name": "the changes name",
"category":"sketch",
"image": "http://whatever.com",
"price": 15000
}
RESPONSE
{
"success": true,
"errorCode": "",
"errorMessage": "",
"data": {},
"traceId": "TB9HW79UT28I2V6P"
}
RESPONSE FAIL
{
"success": false,
"errorCode": "ER0006",
"errorMessage": "name length must greater than 10",
"data": null,
"traceId": "HO6ONN4SICA1UHTY"
}
RESPONSE FAIL
{
"success": false,
"errorCode": "ER0009",
"errorMessage": "item with name 'the first item' already exist",
"data": null,
"traceId": "47KXZPH5MX84YZZU"
}
RESPONSE FAIL
{
"success": false,
"errorCode": "ER0005",
"errorMessage": "word 'sex' is not allowed",
"data": null,
"traceId": "FKSAX0YK1MO6XS82"
}
RESPONSE FAIL
{
"success": false,
"errorCode": "ER0003",
"errorMessage": "invalid category. must be one of [photo sketch cartoon animation]",
"data": null,
"traceId": "NJXGNC72X60GLUBW"
}
RESPONSE FAIL
{
"success": false,
"errorCode": "ER0002",
"errorMessage": "invalid url for 'image'",
"data": null,
"traceId": "91T18FT3SFFFLNVA"
}
RESPONSE FAIL
{
"success": false,
"errorCode": "ER0011",
"errorMessage": "price must greater or equal zero",
"data": null,
"traceId": "FXA7LLI93HOX9HFF"
}
DELETE /api/v1/items/0caf9621-aab4-4fc8-a133-47fc98ec36cf
RESPONSE OK
{
"success": true,
"errorCode": "",
"errorMessage": "",
"data": {},
"traceId": "AHPHIPWUNX147SPN"
}
RESPONSE FAIL
{
"success": false,
"errorCode": "ER0007",
"errorMessage": "unavailable item with id 'abcd9621-aab4-4fc8-a133-47fc98ec61de'",
"data": null,
"traceId": "EH863OJWJUGB2ERC"
}
POST /api/v1/items/de5aafdc-3361-4e69-83ea-5529b21f255e/purchase
REQUEST
{
"quantity": 2
}
RESPONSE OK
{
"success": true,
"errorCode": "",
"errorMessage": "",
"data": {},
"traceId": "V1Z2A4IW93LH79CZ"
}
RESPONSE FAIL
{
"success": false,
"errorCode": "ER0008",
"errorMessage": "unavailable item stock. requested 20 but availability is 10",
"data": null,
"traceId": "Q50VJPFV91UCL9Z1"
}
All error codes listed in domain_item/model/errorenum/error_codes.go
UnknownError ER0000 unknown error
OutOfRangeReputation ER0001 out of range reputation. must between 0 to 1000
InvalidURL ER0002 invalid url for '%s'
InvalidCategory ER0003 invalid category. must be one of %v
InvalidRatingValue ER0004 invalid rating value. must be integer between 0..5
ForbiddenWord ER0005 word '%s' is not allowed
NameLengthMustGreaterThan ER0006 name length must greater than %d
UnavailableItem ER0007 unavailable item with id '%s'
UnavailableItemStock ER0008 unavailable item stock. requested %d but availability is %d
ItemNameAlreadyExist ER0009 item with name '%s' already exist
InvalidReputationBadge ER0010 invalid reputation badge
PriceMustGreaterOrEqualZero ER0011 price must greater or equal zero