- View list of products
- Add/Remove product from cart
- Create new order with payment
- Users can login, sign up
- Cleanly structured and CI integration
- Well-documented
- Well-tested
- Containerized
// From the server's routes, add authentication middleware, using Paseto Token with local symmetric encryption
tokenMaker, _ := token.NewPasetoMaker(config.TokenSymmetricKey)
authRoutes := router.Group("/").Use(authMiddleware(tokenMaker))
// These 6 endpoints need authorization, implements in their handlers repsectively
authRoutes.GET("/users", server.listUsers)
authRoutes.POST("/products", server.createProduct)
authRoutes.POST("/products/cart/add", server.addToCart)
authRoutes.POST("/products/cart/remove", server.removeFromCart)
authRoutes.GET("/orders", server.listOrders)
authRoutes.POST("/orders", server.createOrder)
- An admin user
{"username":"admin",password:"secret"}
have all the powers, is created upon migrate up, log in as admin to test all of the endpoints - A logged-in user can only view their own user's details
- A logged-in user can create a product
- A logged-in user can add a product to cart
- A logged-in user can remove a product from card
- A logged-in user can create an order
- A logged-in user can only view a the orders that they've made
See details
See Booting Up running and testing instructions in the section below first, and then continue:
curl "http://localhost:8080/users" -H "Content-Type: application/json" -d '{"username":"tien1","full_name":"Tien La","email":"tien@email.com","password":"secret"}' | jq
# Should return
{
"username": "tien1",
"full_name": "Tien La",
"email": "tien@email.com",
"password_change_at": "0001-01-01T00:00:00Z",
"created_at": "2021-12-26T18:24:12.73219Z"
}
curl "http://localhost:8080/users/login" -H "Content-Type: application/json" -d '{"username":"tien1","password":"abc123"}' | jq
# Should return (401)
{
"error": "crypto/bcrypt: hashedPassword is not the hash of the given password"
}
After logged-in, copy the access_token
to the TOKEN
variable to be use in appropriated endpoints' -H 'Authorization: Bearer ...'
. Or use Postman/Insomnia/vscode-rest, ...
If having the error token has expired
, log in again
curl "http://localhost:8080/users/login" -H "Content-Type: application/json" -d '{"username":"tien1","password":"secret"}' | jq
# Should return
{
"access_token": "v2.local.iMAQ5gAOXIWxvl446dWq_Z7D7tV_J9MzRQov7HXEi0cbXFU0ZBhsR2GsHlhAeyMbpKMXH8ie-XTW6aKnIFgEfxZNnWXpsUl_QVTsuum1X2H_97UA0iqyP4NEG4JvWdqtrQ30HFN-BdvvXle98eUnKbCFn-28ot60kMGotwRySXJvI-LKCl04crKV31C6yjmKsj-2kPQ14d7eWM7bW8TyDm2DkPy5ZyrmrUTptk3LPLKZSCHPFDa9nfVwO_u4DcG-XZh_Nt6QB3NRTvSwVw.bnVsbA",
"user": {
"username": "tien1",
"full_name": "Tien La",
"email": "tien@email.com",
"password_change_at": "0001-01-01T00:00:00Z",
"created_at": "2021-12-26T18:24:12.73219Z"
}
}
# Set TOKEN variable
TOKEN='v2.local.iMAQ5gAOXIWxvl446dWq_Z7D7tV_J9MzRQov7HXEi0cbXFU0ZBhsR2GsHlhAeyMbpKMXH8ie-XTW6aKnIFgEfxZNnWXpsUl_QVTsuum1X2H_97UA0iqyP4NEG4JvWdqtrQ30HFN-BdvvXle98eUnKbCFn-28ot60kMGotwRySXJvI-LKCl04crKV31C6yjmKsj-2kPQ14d7eWM7bW8TyDm2DkPy5ZyrmrUTptk3LPLKZSCHPFDa9nfVwO_u4DcG-XZh_Nt6QB3NRTvSwVw.bnVsbA'
curl "http://localhost:8080/users?page_id=1&page_size=5" -H "Authorization: Bearer $TOKEN" | jq
# Should return
[
{
"username": "tien1",
"full_name": "Tien La",
"email": "tien@email.com",
"password_change_at": "0001-01-01T00:00:00Z",
"created_at": "2021-12-26T18:24:12.73219Z"
}
]
curl "http://localhost:8080/products?page_id=1&page_size=3" | jq
# Should return
[
{
"id": 1,
"name": "ndomrf",
"cost": 789,
"quantity": 4,
"created_at": "2021-12-26T18:20:27.991534Z"
},
{
"id": 2,
"name": "qsuwja",
"cost": 913,
"quantity": 5,
"created_at": "2021-12-26T18:20:28.05339Z"
},
{
"id": 3,
"name": "jesmsw",
"cost": 754,
"quantity": 9,
"created_at": "2021-12-26T18:20:28.11771Z"
}
]
curl "http://localhost:8080/products/cart/add" -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{"product_id":1,"quantity":2}' | jq
# Should return
{
"product": {
"id": 1,
"name": "ndomrf",
"cost": 789,
"quantity": 2,
"created_at": "2021-12-26T18:20:27.991534Z"
}
}
curl "http://localhost:8080/products/cart/remove" -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{"product_id":1,"quantity":2}' | jq
# Should return
{
"product": {
"id": 1,
"name": "ndomrf",
"cost": 789,
"quantity": 6,
"created_at": "2021-12-26T18:20:27.991534Z"
}
}
curl "http://localhost:8080/orders" -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{"user_id":1,"product_id":1,"quantity":2}' | jq
# Should return
{
"user": {
"username": "tien1",
"full_name": "Tien La",
"email": "tien@email.com",
"password_change_at": "0001-01-01T00:00:00Z",
"created_at": "2021-12-26T18:24:12.73219Z"
},
"product": {
"id": 1,
"name": "ndomrf",
"cost": 789,
"quantity": 2,
"created_at": "2021-12-26T18:20:27.991534Z"
},
"order": {
"id": 24,
"owner": "tien1",
"product_id": 1,
"quantity": 2,
"price": 1578,
"created_at": "2021-12-26T18:26:26.003027Z"
}
}
curl "http://localhost:8080/orders?page_id=1&page_size=5" -H "Authorization: Bearer $TOKEN" | jq
# Should return
[
{
"id": 24,
"owner": "tien1",
"product_id": 1,
"quantity": 2,
"price": 1578,
"created_at": "2021-12-26T18:26:26.003027Z"
}
]
- Go 1.17: Leverage the standard libraries as much as possible
- SQLc: Generates efficient native SQL CRUD code
- PostgreSQL: RDBMS of choice because of faster read due to its indexing model and safer transaction with better isolation levels handling
- Gin: Fast and have respect for native net/http API
- Paseto Token: Better choice than JWT because of enforcing better cryptographic standards and debloated of useless information
- JWT Token: Also implemented to demonstrate the decoupility
- Golang-Migrate: Efficient schema generating, up/down migrating
- GoMock: Generates mocks of about anything
- Docker + Docker-Compose: Containerization, what else to say ...
- Github Actions CI: Make sure we don't push trash code into the codebase
- Viper: Add robustness to configurations
- Adaptive Minimalism: I always keep it as simple as possible, but with a highly decoupled structure we ensure high adaptivity and extensibility, on top of that minimal solid head start. Things are implement only when they're absolutely needed
- Spin up a PostgreSQL instance, run createdb and migrateup:
make network
make postgres
make createdb
make migrateup
- Run test and populate products:
# go get github.com/golang/mock/mockgen/model
# make mock
make test
- Run server:
make server
# make migratedown
make network
make postgres
make createdb
make migrateup
make test
make build
make order-demo
# Rebuild server image
# make build
# make clean
docker compose up -d
# docker compose down
# make clean
# Update Go toolings
go get -u
go mod tidy
# Spin up a container for local development, for example postgres
# The default database will be root, the same name as POSTGRES_USER
docker run --name postgres -p 5432:5432 -e POSTGRES_USER=root -e POSTGRES_PASSWORD=secret -d postgres:alpine
# To access postgres psql
docker exec -it postgres psql -U root
# To access container console and run postgres commands
docker exec -it postgres /bin/sh
# To quit the console
\q
# To view its logs
docker logs postgres
# To stop it
docker stop postgres
# To just run it again
docker start postgres
# To remove it completely
docker rm postgres
Expand
# Go to go.dev/dl and download a binary, in this example it's version 1.17.5
sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.17.5.linux-amd64.tar.gz
# Add these below to your .bashrc or .zshrc
export GOPATH=/home/<username>/go
export GOBIN=/home/<username>/go/bin
export PATH=$PATH:/usr/local/go/bin
export PATH=$PATH:$GOBIN
sudo apt remove docker docker-engine docker.io containerd runc
sudo apt update
sudo apt install apt-transport-https ca-certificates curl gnupg lsb-release software-properties-common
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
echo \
"deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
apt-cache policy docker-ce
sudo apt install docker-ce docker-ce-cli containerd.io
sudo usermod -aG docker $USER
newgrp docker
# Restart the machine then test the installation
docker run hello-world
# On older system you also need to activate the services
sudo systemctl enable docker.service
sudo systemctl enable containerd.service
# Check their github repo for latest version number
sudo curl -L "https://github.com/docker/compose/releases/download/v2.2.2/docker-compose-linux-x86_64" -o /usr/local/bin/docker-compose && sudo chmod +x /usr/local/bin/docker-compose
# To self-update docker-compose
docker-compose migrate-to-labels
go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest
- SQLc:
go install github.com/kyleconroy/sqlc/cmd/sqlc@latest
go install github.com/golang/mock/mockgen@latest
go install https://github.com/spf13/viper@latest
- Gin:
go install github.com/gin-gonic/gin@latest
go get -u github.com/gin-gonic/gin
go get -u github.com/o1egl/paseto
- JWT:
go get -u https://github.com/golang-jwt/jwt
- CURL + JQ + Chocolatery + Make:
sudo apt install curl jq
# These tools are needed only for Windows users
# Run this in an Admin cmd to install Chocolatery first
@"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -InputFormat None -ExecutionPolicy Bypass -Command "[System.Net.ServicePointManager]::SecurityProtocol = 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))" && SET "PATH=%PATH%;%ALLUSERSPROFILE%\chocolatey\bin"
# Then install GNU-Make, cURL, and jq via Chocolatery in Admin pwsh
choco install make curl jq
- Create order-demo-network
make network
- Start postgres container:
make postgres
- Create order_demo database
make createdb
- Run DB migrate up for all versions:
make migrateup
- Run DB migrate down for all versions:
make migratedown
- Generate SQL CRUD via SQLc:
make sqlc
- Generate DB mock via GoMock:
make mock
- Start postgres container:
migrate create -ext sql -dir db/migration -seq <migration_name>