gowebapi

module
v0.0.0-...-7eaaaab Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 12, 2021 License: MIT

README

gowebapi

Go Report Card Build Status Coverage Status

Swagger Validator

Testable Web API in Go with Swagger

This project demonstrates how to structure and build an API using the Go language without a framework. Only carefully chosen packages are included. Dredd is used to test the generated Swagger spec against the API to ensure it's correct.

Older Version: The previous version that was around for a while was 0.1-alpha. If you want to see that code, you can view the tag. The current version is a significant refactor that follows better practices.

Note: You cannot use go get with this repository. You should perform a git clone then set your GOPATH to the folder that git clone created called gowebapi. This allows you to easily fork the repository and build your own applications without rewritting any import paths.

You must use Go 1.7 or newer because this project uses the http context.

Quick Start with Docker Compose

You can build a docker image from this repository and set it up along with a MySQL container using docker compose.

# Create a docker image.
docker build -t webapi:latest .

# Launch MySQL and the webapi with docker compose.
docker-compose up

# Open your browser to http://localhost:8080 and you should be able to access the API.
# Try using the Swagger spec:
# http://petstore.swagger.io/?url=https://raw.githubusercontent.com/josephspurrier/gowebapi/master/src/app/webapi/swagger.json

# Shutdown the containers.
docker-compose down

Manual Start

Use the following commands to start a MySQL container with Docker:

# Start MySQL without a password.
docker run -d --name=mysql57 -p 3306:3306 -e MYSQL_ALLOW_EMPTY_PASSWORD=yes mysql:5.7
# or start MySQL with a password.
docker run -d --name=mysql57 -p 3306:3306 -e MYSQL_ROOT_PASSWORD=somepassword mysql:5.7

# Create the database via docker exec.
docker exec mysql57 sh -c 'exec mysql -uroot -e "CREATE DATABASE IF NOT EXISTS webapi DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci;"'
# Or create the database manually.
CREATE DATABASE webapi DEFAULT CHARSET = utf8 COLLATE = utf8_unicode_ci;

# CD to the CLI tool.
cd src/app/webapi/cmd/cliapp

# Build the CLI tool.
go build

# Apply the database migrations without a password.
DB_USERNAME=root DB_HOSTNAME=127.0.0.1 DB_PORT=3306 DB_DATABASE=webapi ./cliapp migrate all ../../../../../migration/mysql-v0.sql
# or apply the database migrations with a password.
DB_USERNAME=root DB_PASSWORD=somepassword DB_HOSTNAME=127.0.0.1 DB_PORT=3306 DB_DATABASE=webapi ./cliapp migrate all ../../../../../migration/mysql-v0.sql

Using the database connection information above, follow the steps to set up the config.json file:

# Copy the config.json from the root of the project to the CLI app folder.
cp config.json src/app/webapi/cmd/webapi/config.json

# Edit the `Database` section so the connection information matches your MySQL instance.
# The database password is read from the `config.json` file, but is overwritten by the environment variable, `DB_PASSWORD`, if it is set.

# Generate a base64 encoded secret.
./cliapp generate

# Use the encoded secret above to replace the `JWT.Secret` value in the config.

Now you can start the API.

# CD to the webapi app folder.
cd src/app/webapi/cmd/webapi

# Build the app.
go build

# Run the app.
./webapi

# Open your browser to this URL to see the **welcome** message and status **OK**: http://localhost:8080/v1

To interact with the API, open your favorite REST client.

You'll need to authenticate with at http://localhost:8080/v1/auth before you can use any of the user endpoints. Once you have a token, add it to the request header with a name of Authorization and with a value of Bearer {TOKEN HERE}. To create a user, send a POST request to http://localhost:8080/v1/user with the following fields: first_name, last_name, email, and password.

Currently, only a Content-Type of application/x-www-form-urlencoded is supported when sending to the API.

Available Endpoints

The following endpoints are available:

* POST   /v1/user           - Create a new user
* GET	 /v1/user/{user_id} - Retrieve a user by ID
* GET	 /v1/user           - Retrieve a list of all users
* PUT	 /v1/user/{user_id} - Update a user by ID
* DELETE /v1/user/{user_id} - Delete a user by ID
* DELETE /v1/user           - Delete all users

Swagger

This projects uses Swagger v2 to document the API. The entire Swagger spec is generated from the code in this repository.

The Swagger UI linked back to this project can be viewed here.

The Swagger spec JSON file is available here.

Install Swagger

This tool will generate the Swagger spec from annotations in the Go code. It will read the comments in the code and will pull types from structs.

go get github.com/go-swagger/go-swagger/cmd/swagger
Generate Swagger Spec
# CD to the webapi folder.
cd src/app/webapi

# Generate the swagger spec.
swagger generate spec -o ./swagger.json

# Replace 'example' with 'x-example' in the swagger spec.
## MacOS
sed -i '' -e 's/example/x\-example/' ./swagger.json
## Linux
sed -i'' -e 's/example/x\-example/' ./swagger.json

# Validate the swagger spec.
swagger validate ./swagger.json

# Serve the spec for the browser.
swagger serve -F=swagger ./swagger.json

Dredd

This projects uses Dredd to test the Swagger spec against the API. Since the Swagger spec is generated from annotations in the Go code, it's good to ensure there are no discrepancies.

The Go documentation for Dredd is here.

Sample output from Dredd is here.

Install Dredd

You must have MySQL running for these tests to pass.

# Install dredd.
npm install -g dredd

# Get the goodman package for Go hooks.
go get github.com/snikch/goodman/cmd/goodman

# CD to the webapi folder.
cd src/app/webapi

# Copy the testdata/config.json to the current directory.
cp testdata/config.json ./config.json

# Build the hooks app to load the test data.
go build -o ./cmd/hooks/hooks app/webapi/cmd/hooks

# Start MySQL without a password.
docker run -d --name=mysql57 -p 3306:3306 -e MYSQL_ALLOW_EMPTY_PASSWORD=yes mysql:5.7

# Run a test with dredd.
dredd

Vendoring

This project uses dep. The dep init command was run from inside the src/app/webapi folder.

These packages are used in the project:

Folder Structure

All the Go code is inside the src folder. This allows you to easily fork this project to use and test it. You'll just need to set your GOPATH to the gowebapi folder after you do a git clone (don't do a go get, it will not work).

In the src/app/webapi folder, you see a few top level folders:

  • cmd - contains the main function and a static folder for the favicon.
  • component - contains sets of related endpoints and database code.
  • internal - contains project specific packages with dependencies.
  • middleware - contains http wrappers for logging and CORS.
  • model - contains the files with JSON structs that will outputted by the API.
  • pkg - contains generic packages without project specific dependencies - these can be safely moved to other projects without internal dependencies.
  • store - contains the files with SQL used to query the database.

Components

In the root of the src/app/webapi/component folder, you see:

  • core.go - contains the dependencies shared by all the components: logger, database connection, request bind/validation, and the responses.
  • core_mock.go - contains the mocked dependencies which can be used by tests to modify the mocked dependencies.
  • interface.go - contains all the interfaces for the dependencies so you can easily mock out each one for testing purposes.

Inside each component, you see a component.go file which contains the main struct and all the routes. You'll also see individual files for each endpoint with Swagger annotations and the tests for each endpoint.

Store

In the store folder, you see user.go which has the SQL queries. Notice how IDatabase and the IQuery are passed into each store. This provides a unified way to run database queries and also provides a base set of simple SQL queries so you don't have to rewrite them for every table:

  • FindOneByID(dest query.IRecord, ID string) (found bool, err error)
  • FindAll(dest query.IRecord) (total int, err error)
  • ExistsByID(db query.IRecord, s string) (found bool, err error)
  • ExistsByField(db query.IRecord, field string, value string) (found bool, ID string, err error)
  • DeleteOneByID(dest query.IRecord, ID string) (affected int, err error)
  • DeleteAll(dest query.IRecord) (affected int, err error)

This is not an ORM - it just provides you with a simple query builder. Since the struct has an anonymous field, component.IQuery, you can overwrite any of the functions.

For instance, to retrieve a single user from the database, you would use this code:

// Create the store.
u := store.NewUser(p.DB, p.Q)

// Get a user.
exists, err := u.FindOneByID(u, req.UserID)

The code for the generic FindOneByID() is in the pkg/query/query.go file:

// FindOneByID will find a record by string ID.
func (q *Q) FindOneByID(dest IRecord, ID string) (exists bool, err error) {
	err = q.db.Get(dest, fmt.Sprintf(`
		SELECT * FROM %s
		WHERE %s = ?
		LIMIT 1`, dest.Table(), dest.PrimaryKey()),
		ID)
	return recordExists(err)
}

If you wanted to change the query so it excludes deleted users, you could add a new function to the store/user.go file so it looks like this:

// FindOneByID will find a record by string ID excluding deleted records.
func (x *User) FindOneByID(dest query.IRecord, ID string) (exists bool, err error) {
	err = x.db.Get(dest, fmt.Sprintf(`
		SELECT * FROM %s
		WHERE %s = ?
		AND deleted_at  IS NULL
		LIMIT 1`, dest.Table(), dest.PrimaryKey()),
		ID)

	if err == nil {
		return true, nil
	} else if err == sql.ErrNoRows {
		return false, nil
	}
	return false, err
}

This allows you to standardize on how to interact with your database models throughout the team.

Endpoint HTTP Handlers

In order to make the endpoints error driven, all the http handler functions must return an int and an error. This allows error handling to be centralized in the webapi.go file by setting the router.ServeHTTP variable. You can see the routes in the component/user/component.go file:

// Routes will set up the endpoints.
func (p *Endpoint) Routes(router component.IRouter) {
	router.Post("/v1/user", p.Create)
	router.Get("/v1/user/:user_id", p.Show)
	router.Get("/v1/user", p.Index)
	router.Put("/v1/user/:user_id", p.Update)
	router.Delete("/v1/user/:user_id", p.Destroy)
	router.Delete("/v1/user", p.DestroyAll)
}

The endpoints are separated into files under each component folder and they look like this:

func (p *Endpoint) DestroyAll(w http.ResponseWriter, r *http.Request) (int, error) {
	// Create the store.
	u := store.NewUser(p.DB, p.Q)

	// Delete all items.
	count, err := u.DeleteAll(u)
	if err != nil {
		return http.StatusInternalServerError, err
	} else if count < 1 {
		return http.StatusBadRequest, errors.New("no users to delete")
	}

	return p.Response.OK(w, "users deleted")
}

Request Validation

The app/webapi/internal/bind is a wrapper around the github.com/go-playground/validator package so it can validate structs. You can view the user/create.go file to see where the email validation and the required validation is specified in the tags:

// swagger:parameters UserCreate
type request struct {
	// in: formData
	// Required: true
	FirstName string `json:"first_name" validate:"required"`
	// in: formData
	// Required: true
	LastName string `json:"last_name" validate:"required"`
	// in: formData
	// Required: true
	Email string `json:"email" validate:"required,email"`
	// in: formData
	// Required: true
	Password string `json:"password" validate:"required"`
}

// Request validation.
req := new(request)
if err := p.Bind.FormUnmarshal(req, r); err != nil {
	return http.StatusBadRequest, err
} else if err = p.Bind.Validate(req); err != nil {
	return http.StatusBadRequest, err
}

Reflection

The app/webapi/internal/bind and the app/webapi/pkg/structcopy packages use reflection. The bind package will take the form parameters from the request object and map them to a struct. The structcopy package will copy the values from the SQL store structs and set the fields on the JSON model structs based on the JSON tags.

func (p *Endpoint) Index(w http.ResponseWriter, r *http.Request) (int, error) {
	// Create the DB store.
	u := store.NewUser(p.DB, p.Q)

	// Get all items.
	results := make(store.UserGroup, 0)
	_, err := u.FindAll(&results)
	if err != nil {
		return http.StatusInternalServerError, err
	}

	// Copy the items to the JSON model.
	arr := make([]model.UserIndexResponseData, 0)
	for _, u := range results {
		item := new(model.UserIndexResponseData)
		err = structcopy.ByTag(&u, "db", item, "json")
		if err != nil {
			return http.StatusInternalServerError, err
		}
		arr = append(arr, *item)
	}

	// Send the response.
	resp := new(model.UserIndexResponse)
	resp.Body.Status = http.StatusText(http.StatusOK)
	resp.Body.Data = arr
	return p.Response.JSON(w, resp.Body)
}

Logging

You can disable logging on the server by setting an environment variable: WEBAPI_LOG_LEVEL=none

Testing

All the tests use a database called: webapitest. The quickest way to get it set up is:

# Launch MySQL in docker container.
docker run -d --name=mysql57 -p 3306:3306 -e MYSQL_ALLOW_EMPTY_PASSWORD=yes mysql:5.7

# Create the database via docker exec.
docker exec mysql57 sh -c 'exec mysql -uroot -e "CREATE DATABASE IF NOT EXISTS webapitest DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci;"'

# Or create the database manually.
CREATE DATABASE webapitest DEFAULT CHARSET = utf8 COLLATE = utf8_unicode_ci;

You can use these commands to run tests:

# CD to the folder.
cd src/app/webapi

# Test all the packages.
go test ./...

# Get coverage of all tests.
go test -coverpkg=all ./...

# Get the coverage map of the current folder.
go test -coverprofile cover.out && go tool cover -html=cover.out -o cover.html && open cover.html && sleep 5 && rm cover.html && rm cover.out

# Get the coverage map of all the packages.
go test -coverprofile cover.out ./... && go tool cover -html=cover.out -o cover.html && open cover.html && sleep 5 && rm cover.html && rm cover.out

# Get the total code coverage - this only takes into consideration packages that
# have a test file in them.
go test ./... -coverprofile cover.out; go tool cover -func cover.out

Conventions

Rules for mapping HTTP methods to CRUD:

POST   - Create (add record into database)
GET    - Read (get record from the database)
PUT    - Update (edit record in the database)
DELETE - Delete (remove record from the database)

Rules for HTTP status codes:

* Create something            - 201 (Created)
* Read something              - 200 (OK)
* Update something            - 200 (OK)
* Delete something            - 200 (OK)
* Missing request information - 400 (Bad Request)
* Unauthorized operation      - 401 (Unauthorized)
* Any other error             - 500 (Internal Server Error)

Resources

These are all good reads:

Custom HTTP Handlers:

Package Layout:

Directories

Path Synopsis
src
app/webapi
Package webapi Web API
Package webapi Web API
app/webapi/pkg/env
Package env will fill a struct from environment variables.
Package env will fill a struct from environment variables.
app/webapi/pkg/logger
Package logger standardizes the logging functions available to your team.
Package logger standardizes the logging functions available to your team.
app/webapi/pkg/passhash
Package passhash provides password hashing functionality using bcrypt.
Package passhash provides password hashing functionality using bcrypt.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL