This application (boilerplate) is a take on developing a simple Go REST API, backed up by a MongoDb/Embedded Bolt database, using principles of Hexagonal Architecture and SOLID principles.
Note that this is not work done, in any way. For an application to be production ready additional features must be implemented. Some will be added to this repository in the future, other will probably remain the developer's future duties.
- RESTful endpoints (following RFC 7231)
- Standard CRUD operations;
- Error handling (including response JSON generation);
- Clean Architecture code organization (use case centric);
- 3tier application with:
- RESTful API as presentation layer;
- mongoDB or embedded BoltDB as data layer;
- Switch between local (embedded BoltDB) or remote (mongoDB) storages;
- Configurable through YAML files.
Some of the implementation details one can analyze or take note from this application:
- How to load configuration YAMLs using Viper;
- How to specify the ENV variable name in configuration files (loaded with Viper);
- How to setup a configurable logger using Logrus;
- How to setup x-cray/logrus-prefixed-formatter;
- How to organize a use case (feature) in 3 layers;
- How to create a HTTP server using gin-gonic/gin;
- How to implement standard RESTful CRUD operations (RFC 7231);
- How to use mongoDB GO driver (mongodb/mongo-go-driver);
- How to setup and use Storm.
Download modules:
go mod download
Run application locally:
go run ./cmd/api/main.go
Run tests:
go test ./...
.
├── appserver Contains the main application controls implementation;
├── cmd Main applications of the project;
│ └── api The server application API (the entry point);
├── config Configuration logic and configuration files;
├── container Contains the DI container implementation;
├── errors Application errors and error logic;
├── logger Application logger and logic;
├── model Model (entities) definitions and logic;
├── property The entire property use case and dependencies;
│ ├── gateway The gateways implementations;
| | ├── http The HTTP gateways (Controllers);
| | └── storage The storage gateway implementations;
| | ├── bolt The bolt embedded database gateway (Repository);
| | └── mongo The mongoDB gateway (Repository);
│ └── service The property business logic;
├── server The server application logic and dependencies;
│ ├── http The HTTP server implementations;
| └── storage The server's storage overall implementation;
├── tests Contains additional files for testing purposes;
| └── config Configuration files used by config loading tests;
└── util Application overall utilities.
The application configuration is achieved by the ./config/config.yml
file loaded using the Viper module, with a few customizations.
Name | Description |
---|---|
logger.main.level |
The logger level. Accepted values are (case insensitive): panic , fatal , error , warn , warning , info , debug , trace . If none is present the default info is considered. Read more about Logrus Levels. |
logger.main.format |
The logger message format. Accepted values are (case insensitive): text , json . If none is present the default info is considered. |
logger.main.dir |
The directory where all log files are placed. If it does not exist its creation will be attempted. Default value is ./logs . |
logger.main.file-name |
The name of the log file name. Default value is basic-go-rest-api . |
logger.main.prefix |
The default prefix associated with this logger; will be added along with all messages. Default value is main . |
logger.main.with-console |
Boolean value that if true will also print the log messages to console; otherwise the messages can be found only in the log files. Default value is false . |
logger.access.level |
The logger level. Accepted values are (case insensitive): panic , fatal , error , warn , warning , info , debug , trace . If none is present the default info is considered. Read more about Logrus Levels. |
logger.access.format |
The logger message format. Accepted values are (case insensitive): text , json . If none is present the default info is considered. |
logger.access.dir |
The directory where all log files are placed. If it does not exist its creation will be attempted. Default value is ./logs . |
logger.access.file-name |
The name of the log file name. Default value is access . |
logger.access.prefix |
The default prefix associated with this logger; will be added along with all messages. Default value is access . |
logger.access.with-console |
Boolean value that if true will also print the log messages to console; otherwise the messages can be found only in the log files. Default value is false . |
server.http.port |
The port that the server listens on. Default value is 8080 . |
server.http.read-timeout |
The server read timeout (in seconds). Default value is 10 . |
server.http.write-timeout |
The server write timeout (in seconds). Default value is 10 . |
storage.type |
The storage type that must be used. Accepted values are (case insensitive): local , mongo . Default value is local . |
storage.local.name |
The location where the local storage must be created and used from. Default value is local-storage/boltdb . |
storage.mongo.uri |
The mongoDB URI. No default value is provided. |
storage.mongo.name |
The database name. No default value is provided. |
Please see config/config.default.yml
for a full sample and depiction of configuration settings.
logger:
application-log-console: true
server:
http:
port: 8081
storage:
mongo:
uri: "mongodb://localhost:27017"
name: "testdb"
The configuration loading does not use the Viper ENV variables handling but a new approach. Instead of using the name of the property to determine the ENV variable key to load, the property value is used.
Approach in Viper Let's consider the following simple YAML configuration file:
server:
http:
port: 8081
By enabling automatic ENV handling (viper.AutomaticEnv()
), the name of the used variable is $SERVER.HTTP.PORT
.
Local approach Even if the above depicted approach works as expected and it can be customized according to different needs (e.g. adding name prefixes, binding properties or key transformation modules), the exact location from which a property is loaded is not very transparent. Still, for smaller environments and configuration files this might not prove itself to be an issue. But for more complex configuration it can become very difficult to determine which of the properties are loaded from the environment and which from the files. To address this possible maintainability issue, Basic Go REST Api needs the desired ENV variables keys to be specified as the property string value, as it follows:
server:
http:
port: "$APP_SERVER_PORT"
In this way, by following the configuration file, one can have a better view over what properties are loaded from where, as the actual loaded variable can be seen in the configuration file.
-
Controller - exposes the endpoints and that must be its only job. This means that each endpoint must perform an action with 3 steps:
- unmarshall any eventual input (e.g. query parameters, request body);
- call i.e. delegate the action to the business component (in this case the service);
- marshall the response, either error either success.
⚠️ No business logic must be performed at this level, including but not limited to: input business logic validation, business logic, request or compose additional data for the response marshall action. Of course this is not a functional requirement but for code maintainability these rules must be respected. -
Service - implements (or delegates) the business logic with regard to certain models. This is where all the business logic must start from.
-
Repository - implements the gateway towards a certain storage (e.g. mongoDB connector). A repository follows the Adapter Design Patter and it must implement basic operation without taking to much of the responsibility. For example, a repository must not force certain uniqueness on an entity properties if that must be imposed at service level.
In order to implement a new feature (usecase) usually the following steps must be achieved:
- Implement a new service (e.g
property/service/property_service.go
); - Register the service and service creation (e.g
cmd/api/main.go
); - Implement a new repository (e.g
property/gateway/storage/mongo/mongo_repository.go
); - Register the repo and its creation (e.g
cmd/api/main.go
); - Implement controller (e.g
property/gateway/http/controller.go
); - Register the service and service creation (e.g
cmd/api/main.go
).
Even if the project provides a template for feature folder layout, the developer can decide what is the best setup for a particular case. Of course, if the decision is that no services or repositories are required to be implemented, only a Controller must be retrieved.
Pull requests are very welcome. For major changes, please open an issue first to discuss what you would like to change.
Please make sure to update tests as appropriate.