Weather Config REST API - Go REST API
Sample REST Go api to get locations and regions to be used by Ruby on Rails weather website.
Goals of this service
Create sample CRUD REST api in Go which has :
- Go REST api and its dependencies like mongodb, run locally via docker-compose
- Integ testing via newman and docker-compose up postman_checks
- Follow REST naming conventions
- Use swagger for documentation
- Well defined interfaces for model, domain, controller
- All commands can be run via Makefile targets
- Deploy service and swagger docs via docker
- Code coverage, unit and integration tests
Design Patterns / Best practices
Use docker-compose
to build and run go service
, mongodb
, swagger
and newman
# start REST api and its dependency mongodb
docker-compose up app
# run integ tests via newman
docker-compose up postman_check
Use middleware to handle tasks like CORS, logging request performance etc
func (l *Logger) ServeHTTP(w http.ResponseWriter, r *http.Request) {
start := time.Now()
l.handler.ServeHTTP(w, r)
log.Info().Msgf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
}
Use middleware to add unique reqId per request to context
6:07AM INF service/v2/place_service.go:41 > insert location : {ObjectID("000000000000000000000000") north bend snowqualmie_region North Bend 47.497428 -121.786648 db086e5e85941a02ae188f726f7e9e2c}
reqId=74590ef0-2c7b-4e62-bfef-03ae13fa85a3
6:07AM INF service/v2/place_service.go:47 > inserted location with id : 5f30e4046c7600d75072329d reqId=74590ef0-2c7b-4e62-bfef-03ae13fa85a3
6:07AM INF middleware/request_logger.go:19 > POST /v2/locations 3.15215768s reqId=74590ef0-2c7b-4e62-bfef-03ae13fa85a3
Follow REST naming convention and swagger documentation style
api.HandleFunc("/locations", placesCtrl.FindLocations).Methods("GET")
api.HandleFunc("/regions", placesCtrl.CreateRegion).Methods(http.MethodPost)
api.HandleFunc("/region/{id:[0-9a-zA-Z]+}", placesCtrl.GetRegion).Methods(http.MethodGet)
api.HandleFunc("/region/{id:[0-9a-zA-Z]+}", placesCtrl.UpdateRegion).Methods(http.MethodPut)
api.HandleFunc("/region/{id:[0-9a-zA-Z]+}", placesCtrl.DeleteRegion).Methods(http.MethodDelete)
Rest controller(s) expose endpoints via PlaceController interface
type PlaceController interface {
FindLocations(w http.ResponseWriter, r *http.Request)
FindRegions(w http.ResponseWriter, r *http.Request)
}
Handle errors and return the correct http response codes
if errors.Is(err, v2.InvalidInputError) {
log.Info().Msg(err.Error())
w.WriteHeader(http.StatusBadRequest)
}
Service package handles business logic via PlaceService interface
type PlaceService interface {
GetLocations() ([]model.Location, error)
GetRegions() ([]model.Region, error)
}
type NewPlaceServiceImpl struct {
client client.StorageClient
}
Client package handles external clients
type StorageClient interface {
QueryLocations(dataDir string) (map[string]model.Location, error)
QueryRegions(dataDir string) (map[string]model.Region, error)
}
Model pacakge contains Data Transfer Objects (DTOs)
type Region struct {
Name string `json:"name"`
SearchKey string `json:"searchKey"`
Description string `json:"description"`
}
Testing
- integ testing using local mongo, postman and newman
docker-compose -f docker-compose.yml -f docker-compose.test.yml up postman_checks
# to install
go get github.com/golang/mock/gomock
go get github.com/golang/mock/mockgen
mockgen -source=controller/place_controller.go -destination=controller/place_controller_mock.go -package=controller
- example integration test :
func TestGetLocations(t *testing.T) {
req, err := http.NewRequest("GET", "/locations", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
storageClient := client.NewStorageClient("../data")
svc := service.NewPlaceService(storageClient)
placesCtrl := controller.NewPlaceController(svc)
handler := http.HandlerFunc(placesCtrl.FindLocations)
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Fatalf("handler returned wrong status code: got %v want %v",
status, http.StatusOK)
}
expected := 26
var result *[]model.Location
err = json.Unmarshal(rr.Body.Bytes(), &result)
if err != nil {
t.Fatalf("error unmarshaling results : %v", err)
}
if len(*result) != expected {
t.Fatalf("got %v want %v",
len(*result), expected)
}
}
we use swagger to generate living documentation
https://goswagger.io/
build / generate mocks / run tests is via Makefile
targets
https://github.com/gadzooks/weather-go-api/blob/master/Makefile
dependencies
we use dep
to mange dependencies
code coverage via go test
https://github.com/gadzooks/weather-go-api/blob/master/coverage.out
Todo
- postman and newman tests
- deploy with kubernetes, alongside other apps
- use different envs : dev, staging, prod
- JWT auth ? for service to service authentication
- connecting to mongodb to get props