-
Resource
(or Entity): An object representing a resource as defined in our domain model, e.g.Product
,Purchaser
, etc. -
Controller
: A set ofEndpoints
(or routes or route handlers) that deal mainly with receiving and sending payloads in various formats, e.g. JSON.Controllers
represent our endpoint layer. -
Service
: A set of related functions that implement our business logic.Services
represent our service layer. -
Model
(or Store): A set of related functions that deal with creating, reading, updating and deleting oneResource
.Models
represent our data layer.
/cmd # Contains bootstraps for all our executables, i.e. the things we build and put in docker containers to run.
/wpark # Main folder that contains all our Go code.
/wpark/apiserver # The web layer. This server is used to expose `Endpoints` over HTTPS / TLS.
/wpark/config # The Core API config.
/wpark/controller # Contains all our `Controllers`, e.g. `ProductController`, `PurchaserController`, etc. This constitutes our endpoint layer.
/wpark/core # Core API root package and domain model. Contains all interface definitions (services, models, payloads, etc.) and constructor functions for all resources.
/wpark/mysql # MySQL implementation of our data layer (implementation of all model interfaces defined in /wpark/core).
/wpark/e2e # All our end-to-end tests.
/wpark/mock # Mock implementations of all services and models defined in /wpark/core.
/wpark/pkg/* # Various standalone packages, e.g. /wpark/pkg/logger, which handles logging.
/wpark/service # All service implementations, e.g `ProductService`, `PurchaserService`, etc. This constitutes our service layer.
- MySQL >= v5.7.23 - data store.
This section outlines the rules that govern the design of the system.
-
Dependency Injection (DI): To keep things modular, easy to test, and to separate concerns, we want to be coding to interfaces, and injecting implementations of those interfaces into our
Controllers
andServices
as part of our bootstrap. Using DI we're able to mock all our dependencies and unit test each layer in isolation. It's also trivial to swap out one implementation for another whenever we want. -
Endpoint Layer:
Controllers
make up our endpoint layer. AController
is basically just a set of handlers (e.g route handlers).Controllers
normally depend on one or moreServices
to do their job.A typical handler (
Controller
method) should ideally do only the following:- Receive a payload (e.g. JSON or URL parameters) over HTTPS and validate the payload against a validation schema, e.g. parse an incoming JSON payload into a valid
CreateProductRequestV1
struct. - Pass validated data to a
Service
and wait for a response, e.g. by callingProductService.CreateProduct(...)
. - Create a versioned endpoint response, based on the
Service
response, and send it back to the caller, e.g create and serialize aCreateProductResponseV1
struct.
AVOID doing the following:
- Inject
Models
into aController
and use thoseModels
directly. For example, callingProductModel.Update(...)
directly from within aController
, instead of going throughProductService
, is considered wrong. It's important to note that aModel
is owned by one and only oneService
, and only thatService
ever gets to call it'sModel
.
- Receive a payload (e.g. JSON or URL parameters) over HTTPS and validate the payload against a validation schema, e.g. parse an incoming JSON payload into a valid
-
Service Layer:
Services
contain all our business logic.Services
own one or moreModels
, and may depend on one or more otherServices
to do their job. -
Data Layer:
Models
make up our data layer. AModel
implements the CRUD operations for aResource
and should not contain any business logic.
We draw inspiration from the following APIs and documents:
- Go Project layout(by Ben Johnson): https://medium.com/@benbjohnson/standard-package-layout-7cdbc8391fc1
- Google Cloud API: https://cloud.google.com/apis/design/
This section outlines how to run tests.
We use go modules
for dependency management. Using Go Modules
As of Go 1.11, the go command enables the use of modules when the current directory or any parent directory has a go.mod, provided the directory is outside $GOPATH/src. (Inside $GOPATH/src, for compatibility, the go command still runs in the old GOPATH mode, even if a go.mod is found. See the go command documentation for details.) Starting in Go 1.13, module mode will be the default for all development.
As noted above, please make sure that you checkout this repository outside of $GOPATH
. Since we are using go modules
we do not need to explicitly download the dependencies, go build
or go test
would take care of it.
We use go test
to run our tests and show us our test coverage in the output.
-
Run unit tests.
bash test.sh
This will run all the unit tests in isolation with mocked dependencies.
Pasting output here for convenience:Running unit tests .. ok github.com/.../wpark/core 0.002s coverage: 89.5% of statements ok github.com/.../wpark/service 0.003s coverage: 90.3% of statements ok github.com/.../wpark/apiserver 0.006s coverage: 56.8% of statements ok github.com/.../wpark/controller 0.007s coverage: 92.3% of statements Done.
-
Run end-to-end tests.
bash test.sh -e2e
This will run all the unit tests along with tests that require real external dependencies like the data layer. Before running end-to-end tests, make sure that our data layer provider, mysql in this case, is up and running.
docker-compose up
Pasting output here for convenience:
Running unit tests .. ok github.com/.../wpark/core 0.003s coverage: 89.5% of statements ok github.com/.../wpark/service 0.003s coverage: 90.3% of statements ok github.com/.../wpark/apiserver 0.006s coverage: 56.8% of statements ok github.com/.../wpark/controller 0.007s coverage: 92.3% of statements Running e2e tests .. ok github.com/.../wpark/mysql 0.341s coverage: 72.0% of statements ok github.com/.../wpark/e2e 0.111s coverage: 77.5% of statements Done.
To run the api server and listen on our endpoints, run the following command:
-
Make sure that our data layer provider, mysql in this case, is up and running.
docker-compose up
-
Initialize and create our tables in mysql the first time.
go run cmd/wpark-api/main.go --initdb
-
Run our api server.
Open a new terminal window, and run the following command.go run cmd/wpark-api/main.go
This will start the api server and log all request information in that terminal window.
By default it will run on http://localhost:11111
You can override the default variables by looking at various env variables defined inconfig-example.sh
file.