Golang CRUD service for DVD store that communicates by GRPC and follows the principles of Clean Architecture by Robert Martin.
It has simplified business logic in order to concentrate on architecture, code organization and practicing GRPC.
Project structure (mostly) follows Standard Go Project Layout.
/cmd
- entry point of the app/config
- configuration/internal/dvdstore
- application code (interfaces, transports, implementations)/internal/dvdstore/grpc
- GRPC transport/internal/dvdstore/repository
- working with repositories, currently only postgresql/internal/dvdstore/usecase
- business logic/internal/models
- entities, exported errors, custom validations/internal/server
- initialization of the app ("continues" main.go)/pkg/postgres
- postgres connection config/proto
- protobuf definition and proto-generated code
To follow dependency inversion, use cases and repositories are described through interfaces.
Concrete repository implementations realize communication with needed data sources, in this project it is postgresql.
Concrete use case implementations aggregate repository interface; transport (grpc) aggregates use case interface.
Such code organization simplifies unit testing and allows us to make code flexible - we can easily add/switch between data sources and transports, write different use cases.
Transport receives request from client and calls use case. Use case validates the request and calls repository. Repository retrieves data from data source and forms entity. Entity is mapped to response structure and returned to client with status code. If any error appears, the app returns corresponding error with error code.
Project uses Dell DVD store database:
Some of the tables are ignored to simplify the business logic.
# Build and start container with database
docker compose up -d
# Load dependencies
go mod tidy
# Run the app
go run cmd/main.go
If everything is ok, you will see this message:
{"level":"info","msg":"GRPC listening on port 9090"}
You can use any preferred GRPC client to call API, for example grpcurl or Postman.
Service uses reflection, so you can describe it through the client.
# describe service
grpcurl -plaintext localhost:9090 describe
# describe message
grpcurl -plaintext localhost:9090 describe proto.GetCustomerReq
# request customer orders
grpcurl -d '{"CustomerID": 268}' -plaintext localhost:9090 proto.Dvdstore/GetCustomerOrders
Though I recommend to use Postman.
GetCustomers returns list of all Customers limited by provided limit
Request | Response |
---|---|
{
"Limit": 2
} |
{
"CustomerList": [
{
"Id": "2",
"FirstName": "HQNMZH",
"LastName": "UNUKXHJVXB",
"Age": "80"
},
{
"Id": "3",
"FirstName": "JTNRNB",
"LastName": "LYYSHTQJRE",
"Age": "47"
}
]
} |
GetCustomer returns Customer by provided id
Request | Response |
---|---|
{
"CustomerID": 268
} |
{
"Customer": {
"Id": "268",
"FirstName": "MKZPVX",
"LastName": "CBIHNABLQI",
"Age": "54"
}
} |
AddCustomer adds passed Customer and returns his id. Passed customer "Id" field is ignored
Request | Response |
---|---|
{
"Customer": {
"Age": 30,
"FirstName": "John",
"LastName": "Doe"
}
} |
{
"CustomerID": "20003"
} |
DeleteCustomer deletes Customer by provided id. Returns empty response if no errors were met
Request | Response |
---|---|
{
"CustomerID": 16
} |
{} |
GetProducts returns list of all Products limited by provided limit
Request | Response |
---|---|
{
"Limit": 2
} |
{
"ProductList": [
{
"Id": "1",
"Title": "ACADEMY ACADEMY",
"Price": 25.99,
"Quantity": "138"
},
{
"Id": "2",
"Title": "ACADEMY ACE",
"Price": 20.99,
"Quantity": "118"
}
]
} |
GetProduct returns Product by provided id
Request | Response |
---|---|
{
"ProductID": 17
} |
{
"Product": {
"Id": "17",
"Title": "ACADEMY ALONE",
"Price": 28.99,
"Quantity": "114"
}
} |
AddProduct adds passed Product and returns his id. Passed product "Id" field is ignored
Request | Response |
---|---|
{
"Product": {
"Price": 10.55,
"Quantity": 6,
"Title": "Product"
}
} |
{
"ProductID": "10006"
} |
DeleteProduct deletes Product by provided id. Returns empty response if no errors were met
Request | Response |
---|---|
{
"ProductID": 58
} |
{} |
GetOrder gets order by provided id
Request | Response |
---|---|
{
"OrderID": 54
} |
{
"Order": {
"Id": "54",
"Date": {
"seconds": "1074124800"
},
"NetAmount": 311.01,
"Tax": 25.66,
"TotalAmount": 336.67,
"ProductList": [
{
"Id": "5787",
"Title": "AGENT SHINING",
"Price": 9.99,
"Quantity": "3"
}
]
}
} |
GetCustomerOrders returns customer orders by provided customer id
Request | Response |
---|---|
{
"CustomerID": 359
} |
{
"OrderList": [
{
"Id": "7453",
"Date": {
"seconds": "1091836800"
},
"NetAmount": 124.11,
"Tax": 10.24,
"TotalAmount": 134.35,
"ProductList": [
{
"Id": "7114",
"Title": "AIRPORT CAMELOT",
"Price": 9.99,
"Quantity": "3"
}
]
}
]
} |
AddOrder adds order for passed customer id with provided products and returns created order id.
"Title" and "Price" fields in passed ProductList are ignored
Request | Response |
---|---|
{
"CustomerID": 36,
"ProductList": [
{
"Id": 34,
"Quantity": 2
},
{
"Id": 92,
"Quantity": 10
}
]
} |
{
"OrderID": "12010"
} |
DeleteOrder deletes order with provided order id. Returns empty response if no errors were met
Request | Response |
---|---|
{
"OrderID": 14
} |
{} |