{"id":25422389,"url":"https://github.com/lavantien/order-demo","last_synced_at":"2026-04-11T03:33:59.179Z","repository":{"id":45262235,"uuid":"440232088","full_name":"lavantien/order-demo","owner":"lavantien","description":"A production-ready simple order service demo","archived":false,"fork":false,"pushed_at":"2026-02-19T00:32:06.000Z","size":406,"stargazers_count":6,"open_issues_count":0,"forks_count":2,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-02-19T06:47:23.996Z","etag":null,"topics":["docker","docker-compose","gin","golang","gomock","migrate","order","paseto","postgres","sqlc","token","viper"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lavantien.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2021-12-20T16:15:11.000Z","updated_at":"2026-02-19T00:32:10.000Z","dependencies_parsed_at":"2025-02-16T21:33:08.528Z","dependency_job_id":"51e7171c-6e46-4d20-89a5-efa714a9da48","html_url":"https://github.com/lavantien/order-demo","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/lavantien/order-demo","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lavantien%2Forder-demo","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lavantien%2Forder-demo/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lavantien%2Forder-demo/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lavantien%2Forder-demo/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lavantien","download_url":"https://codeload.github.com/lavantien/order-demo/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lavantien%2Forder-demo/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31668049,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-10T17:19:37.612Z","status":"online","status_checked_at":"2026-04-11T02:00:05.776Z","response_time":54,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["docker","docker-compose","gin","golang","gomock","migrate","order","paseto","postgres","sqlc","token","viper"],"created_at":"2025-02-16T21:33:05.824Z","updated_at":"2026-04-11T03:33:59.136Z","avatar_url":"https://github.com/lavantien.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Production-ready Simple Order Service Demo\n\n![ci-test](https://github.com/lavantien/order-demo/actions/workflows/ci.yml/badge.svg?branch=main)\n\n## 4 Requirements and 4 Bonuses\n\n1. [X] View list of products\n2. [X] Add/Remove product from cart\n3. [X] Create new order with payment\n4. [X] Users can login, sign up\n\n----\n\n1. [X] Cleanly structured and CI integration\n2. [X] Well-documented\n3. [X] Well-tested\n4. [X] Containerized\n\n### **Authorization Rules**\n\n```go\n// From the server's routes, add authentication middleware, using Paseto Token with local symmetric encryption\ntokenMaker, _ := token.NewPasetoMaker(config.TokenSymmetricKey)\nauthRoutes := router.Group(\"/\").Use(authMiddleware(tokenMaker))\n// These 6 endpoints need authorization, implements in their handlers repsectively\nauthRoutes.GET(\"/users\", server.listUsers)\nauthRoutes.POST(\"/products\", server.createProduct)\nauthRoutes.POST(\"/products/cart/add\", server.addToCart)\nauthRoutes.POST(\"/products/cart/remove\", server.removeFromCart)\nauthRoutes.GET(\"/orders\", server.listOrders)\nauthRoutes.POST(\"/orders\", server.createOrder)\n```\n\n0. [X] 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\n1. [X] A logged-in user can only view their own user's details\n2. [X] A logged-in user can create a product\n3. [X] A logged-in user can add a product to cart\n4. [X] A logged-in user can remove a product from card\n5. [X] A logged-in user can create an order\n6. [X] A logged-in user can only view a the orders that they've made\n\n### **API Endpoints**\n\n\u003cdetails\u003e\n\t\u003csummary\u003eSee details\u003c/summary\u003e\n\nSee **Booting Up** running and testing instructions in the section below first, and then continue:\n\n![Server running](/resources/readme/server-running.png \"Server running\")\n\n### Rqm4.1: Create user via endpoint\n\n```bash\ncurl \"http://localhost:8080/users\" -H \"Content-Type: application/json\" -d '{\"username\":\"tien1\",\"full_name\":\"Tien La\",\"email\":\"tien@email.com\",\"password\":\"secret\"}' | jq\n```\n\n```json\n# Should return\n{\n  \"username\": \"tien1\",\n  \"full_name\": \"Tien La\",\n  \"email\": \"tien@email.com\",\n  \"password_change_at\": \"0001-01-01T00:00:00Z\",\n  \"created_at\": \"2021-12-26T18:24:12.73219Z\"\n}\n```\n\n### Rqm4.2: User login with wrong password\n\n```bash\ncurl \"http://localhost:8080/users/login\" -H \"Content-Type: application/json\" -d '{\"username\":\"tien1\",\"password\":\"abc123\"}' | jq\n```\n\n```json\n# Should return (401)\n{\n  \"error\": \"crypto/bcrypt: hashedPassword is not the hash of the given password\"\n}\n```\n\n### Rqm4.3: User log in\n\nAfter 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, ...\n\nIf having the error `token has expired`, log in again\n\n```bash\ncurl \"http://localhost:8080/users/login\" -H \"Content-Type: application/json\" -d '{\"username\":\"tien1\",\"password\":\"secret\"}' | jq\n```\n\n```json\n# Should return\n{\n  \"access_token\": \"v2.local.iMAQ5gAOXIWxvl446dWq_Z7D7tV_J9MzRQov7HXEi0cbXFU0ZBhsR2GsHlhAeyMbpKMXH8ie-XTW6aKnIFgEfxZNnWXpsUl_QVTsuum1X2H_97UA0iqyP4NEG4JvWdqtrQ30HFN-BdvvXle98eUnKbCFn-28ot60kMGotwRySXJvI-LKCl04crKV31C6yjmKsj-2kPQ14d7eWM7bW8TyDm2DkPy5ZyrmrUTptk3LPLKZSCHPFDa9nfVwO_u4DcG-XZh_Nt6QB3NRTvSwVw.bnVsbA\",\n  \"user\": {\n    \"username\": \"tien1\",\n    \"full_name\": \"Tien La\",\n    \"email\": \"tien@email.com\",\n    \"password_change_at\": \"0001-01-01T00:00:00Z\",\n    \"created_at\": \"2021-12-26T18:24:12.73219Z\"\n  }\n}\n\n# Set TOKEN variable\nTOKEN='v2.local.iMAQ5gAOXIWxvl446dWq_Z7D7tV_J9MzRQov7HXEi0cbXFU0ZBhsR2GsHlhAeyMbpKMXH8ie-XTW6aKnIFgEfxZNnWXpsUl_QVTsuum1X2H_97UA0iqyP4NEG4JvWdqtrQ30HFN-BdvvXle98eUnKbCFn-28ot60kMGotwRySXJvI-LKCl04crKV31C6yjmKsj-2kPQ14d7eWM7bW8TyDm2DkPy5ZyrmrUTptk3LPLKZSCHPFDa9nfVwO_u4DcG-XZh_Nt6QB3NRTvSwVw.bnVsbA'\n```\n\n### Rqm4.4: List users\n\n```bash\ncurl \"http://localhost:8080/users?page_id=1\u0026page_size=5\" -H \"Authorization: Bearer $TOKEN\" | jq\n```\n\n```json\n# Should return\n[\n  {\n    \"username\": \"tien1\",\n    \"full_name\": \"Tien La\",\n    \"email\": \"tien@email.com\",\n    \"password_change_at\": \"0001-01-01T00:00:00Z\",\n    \"created_at\": \"2021-12-26T18:24:12.73219Z\"\n  }\n]\n```\n\n### Rqm1: View list of products\n\n```bash\ncurl \"http://localhost:8080/products?page_id=1\u0026page_size=3\" | jq\n```\n\n```json\n# Should return\n[\n  {\n    \"id\": 1,\n    \"name\": \"ndomrf\",\n    \"cost\": 789,\n    \"quantity\": 4,\n    \"created_at\": \"2021-12-26T18:20:27.991534Z\"\n  },\n  {\n    \"id\": 2,\n    \"name\": \"qsuwja\",\n    \"cost\": 913,\n    \"quantity\": 5,\n    \"created_at\": \"2021-12-26T18:20:28.05339Z\"\n  },\n  {\n    \"id\": 3,\n    \"name\": \"jesmsw\",\n    \"cost\": 754,\n    \"quantity\": 9,\n    \"created_at\": \"2021-12-26T18:20:28.11771Z\"\n  }\n]\n```\n\n### Rqm2.1: Add product to cart\n\n```bash\ncurl \"http://localhost:8080/products/cart/add\" -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"product_id\":1,\"quantity\":2}' | jq\n```\n\n```json\n# Should return\n{\n  \"product\": {\n    \"id\": 1,\n    \"name\": \"ndomrf\",\n    \"cost\": 789,\n    \"quantity\": 2,\n    \"created_at\": \"2021-12-26T18:20:27.991534Z\"\n  }\n}\n```\n\n### Rqm2.2: Remove product from cart\n\n```bash\ncurl \"http://localhost:8080/products/cart/remove\" -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"product_id\":1,\"quantity\":2}' | jq\n```\n\n```json\n# Should return\n{\n  \"product\": {\n    \"id\": 1,\n    \"name\": \"ndomrf\",\n    \"cost\": 789,\n    \"quantity\": 6,\n    \"created_at\": \"2021-12-26T18:20:27.991534Z\"\n  }\n}\n```\n\n### Rqm3.1: Create new order with payment\n\n```bash\ncurl \"http://localhost:8080/orders\" -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"user_id\":1,\"product_id\":1,\"quantity\":2}' | jq\n```\n\n```json\n# Should return\n{\n  \"user\": {\n    \"username\": \"tien1\",\n    \"full_name\": \"Tien La\",\n    \"email\": \"tien@email.com\",\n    \"password_change_at\": \"0001-01-01T00:00:00Z\",\n    \"created_at\": \"2021-12-26T18:24:12.73219Z\"\n  },\n  \"product\": {\n    \"id\": 1,\n    \"name\": \"ndomrf\",\n    \"cost\": 789,\n    \"quantity\": 2,\n    \"created_at\": \"2021-12-26T18:20:27.991534Z\"\n  },\n  \"order\": {\n    \"id\": 24,\n    \"owner\": \"tien1\",\n    \"product_id\": 1,\n    \"quantity\": 2,\n    \"price\": 1578,\n    \"created_at\": \"2021-12-26T18:26:26.003027Z\"\n  }\n}\n```\n\n### Rqm3.2: Check the result\n\n```bash\ncurl \"http://localhost:8080/orders?page_id=1\u0026page_size=5\" -H \"Authorization: Bearer $TOKEN\" | jq\n```\n\n```json\n# Should return\n[\n  {\n    \"id\": 24,\n    \"owner\": \"tien1\",\n    \"product_id\": 1,\n    \"quantity\": 2,\n    \"price\": 1578,\n    \"created_at\": \"2021-12-26T18:26:26.003027Z\"\n  }\n]\n```\n\n\u003c/details\u003e\n\n## Database UML\n\n![Database UML](/resources/readme/order-demo.png \"Database UML\")\n\n## Technology Stack\n\n- **Go 1.17**: *Leverage the standard libraries as much as possible*\n- **SQLc**: *Generates efficient native SQL CRUD code*\n- **PostgreSQL**: *RDBMS of choice because of faster read due to its indexing model and safer transaction with better isolation levels handling*\n- **Gin**: *Fast and have respect for native net/http API*\n- **Paseto Token**: *Better choice than JWT because of enforcing better cryptographic standards and debloated of useless information*\n- **JWT Token**: *Also implemented to demonstrate the decoupility*\n- **Golang-Migrate**: *Efficient schema generating, up/down migrating*\n- **GoMock**: *Generates mocks of about anything*\n- **Docker** + **Docker-Compose**: *Containerization, what else to say ...*\n- **Github Actions CI**: *Make sure we don't push trash code into the codebase*\n- **Viper**: *Add robustness to configurations*\n\n## Philosophy and Architecture\n\n- **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*\n\n## Booting Up\n\n### Non-docker way\n\n- Spin up a PostgreSQL instance, run createdb and migrateup:\n\n```bash\nmake network\n\nmake postgres\n\nmake createdb\n\nmake migrateup\n```\n\n- Run test and populate products:\n\n```bash\n# go get github.com/golang/mock/mockgen/model\n\n# make mock\n\nmake test\n```\n\n- Run server:\n\n```bash\nmake server\n\n# make migratedown\n```\n\n### Docker way\n\n```bash\nmake network\n\nmake postgres\n\nmake createdb\n\nmake migrateup\n\nmake test\n\nmake build\n\nmake order-demo\n\n# Rebuild server image\n# make build\n\n# make clean\n```\n\n### Docker Compose way\n\n```bash\ndocker compose up -d\n\n# docker compose down\n\n# make clean\n```\n\n## Development Infrastructure Setup\n\n### Helpful Commands\n\n```bash\n# Update Go toolings\ngo get -u\ngo mod tidy\n\n# Spin up a container for local development, for example postgres\n# The default database will be root, the same name as POSTGRES_USER\ndocker run --name postgres -p 5432:5432 -e POSTGRES_USER=root -e POSTGRES_PASSWORD=secret -d postgres:alpine\n\n# To access postgres psql\ndocker exec -it postgres psql -U root\n\n# To access container console and run postgres commands\ndocker exec -it postgres /bin/sh\n\n# To quit the console\n\\q\n\n# To view its logs\ndocker logs postgres\n\n# To stop it\ndocker stop postgres\n\n# To just run it again\ndocker start postgres\n\n# To remove it completely\ndocker rm postgres\n```\n\n### Tooling Installation Guide\n\n\u003cdetails\u003e\n    \u003csummary\u003eExpand\u003c/summary\u003e\n\n- [**Golang**](https://go.dev/doc/install):\n\n```bash\n# Go to go.dev/dl and download a binary, in this example it's version 1.17.5\n\nsudo rm -rf /usr/local/go \u0026\u0026 sudo tar -C /usr/local -xzf go1.17.5.linux-amd64.tar.gz\n\n# Add these below to your .bashrc or .zshrc\nexport GOPATH=/home/\u003cusername\u003e/go\nexport GOBIN=/home/\u003cusername\u003e/go/bin\nexport PATH=$PATH:/usr/local/go/bin\nexport PATH=$PATH:$GOBIN\n```\n\n- [**Docker**](https://docs.docker.com/engine/install/ubuntu/):\n\n```bash\nsudo apt remove docker docker-engine docker.io containerd runc\n\nsudo apt update\n\nsudo apt install apt-transport-https ca-certificates curl gnupg lsb-release software-properties-common\n\ncurl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg\n\necho \\\n  \"deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \\\n  $(lsb_release -cs) stable\" | sudo tee /etc/apt/sources.list.d/docker.list \u003e /dev/null\n\nsudo apt update\n\napt-cache policy docker-ce\n\nsudo apt install docker-ce docker-ce-cli containerd.io\n\nsudo usermod -aG docker $USER\n\nnewgrp docker\n\n# Restart the machine then test the installation\n\ndocker run hello-world\n\n# On older system you also need to activate the services\n\nsudo systemctl enable docker.service\n\nsudo systemctl enable containerd.service\n```\n\n- [**Docker-Compose**](https://docs.docker.com/compose/install/):\n\n```bash\n# Check their github repo for latest version number\nsudo curl -L \"https://github.com/docker/compose/releases/download/v2.2.2/docker-compose-linux-x86_64\" -o /usr/local/bin/docker-compose \u0026\u0026 sudo chmod +x /usr/local/bin/docker-compose\n\n# To self-update docker-compose\ndocker-compose migrate-to-labels\n```\n\n- [**Golang-Migrate**](https://github.com/golang-migrate/migrate/tree/master/cmd/migrate):\n\n```bash\ngo install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest\n```\n\n- [**SQLc**](https://docs.sqlc.dev/en/latest/overview/install.html):\n\n```bash\ngo install github.com/kyleconroy/sqlc/cmd/sqlc@latest\n```\n\n- [**GoMock**](https://github.com/golang/mock):\n\n```bash\ngo install github.com/golang/mock/mockgen@latest\n```\n\n- [**Viper**](https://github.com/spf13/viper):\n\n```bash\ngo install https://github.com/spf13/viper@latest\n```\n\n- [**Gin**](https://github.com/gin-gonic/gin#installation):\n\n```bash\ngo install github.com/gin-gonic/gin@latest\n\ngo get -u github.com/gin-gonic/gin\n```\n\n- [**Paseto**](https://github.com/o1egl/paseto):\n\n```bash\ngo get -u github.com/o1egl/paseto\n```\n\n- [**JWT**](https://github.com/golang-jwt/jwt):\n\n```bash\ngo get -u https://github.com/golang-jwt/jwt\n```\n\n- [**CURL**](https://curl.se/download.html) + [**JQ**](https://stedolan.github.io/jq/) + [**Chocolatery**](https://docs.chocolatey.org/en-us/choco/setup) + [**Make**](https://community.chocolatey.org/packages/make):\n\n```bash\nsudo apt install curl jq\n\n# These tools are needed only for Windows users\n\n# Run this in an Admin cmd to install Chocolatery first\n@\"%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'))\" \u0026\u0026 SET \"PATH=%PATH%;%ALLUSERSPROFILE%\\chocolatey\\bin\"\n\n# Then install GNU-Make, cURL, and jq via Chocolatery in Admin pwsh\nchoco install make curl jq\n```\n\n### Infrastructure\n\n- Create order-demo-network\n\n```bash\nmake network\n```\n\n- Start postgres container:\n\n```bash\nmake postgres\n```\n\n- Create order_demo database\n\n```bash\nmake createdb\n```\n\n- Run DB migrate up for all versions:\n\n```bash\nmake migrateup\n```\n\n- Run DB migrate down for all versions:\n\n```bash\nmake migratedown\n```\n\n### Code Generation\n\n- Generate SQL CRUD via SQLc:\n\n```bash\nmake sqlc\n```\n\n- Generate DB mock via GoMock:\n\n```bash\nmake mock\n```\n\n- Start postgres container:\n\n```bash\nmigrate create -ext sql -dir db/migration -seq \u003cmigration_name\u003e\n```\n\n\u003c/details\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flavantien%2Forder-demo","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flavantien%2Forder-demo","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flavantien%2Forder-demo/lists"}