{"id":37150549,"url":"https://github.com/josephspurrier/gowebapi","last_synced_at":"2026-01-14T17:47:26.333Z","repository":{"id":57558720,"uuid":"58348798","full_name":"josephspurrier/gowebapi","owner":"josephspurrier","description":"Testable Web API in Go with Swagger","archived":false,"fork":false,"pushed_at":"2018-09-22T00:10:11.000Z","size":648,"stargazers_count":57,"open_issues_count":1,"forks_count":17,"subscribers_count":8,"default_branch":"master","last_synced_at":"2024-06-20T16:33:31.191Z","etag":null,"topics":["api","go","golang"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/josephspurrier.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2016-05-09T04:40:22.000Z","updated_at":"2023-09-15T02:09:43.000Z","dependencies_parsed_at":"2022-09-15T19:01:30.967Z","dependency_job_id":null,"html_url":"https://github.com/josephspurrier/gowebapi","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/josephspurrier/gowebapi","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/josephspurrier%2Fgowebapi","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/josephspurrier%2Fgowebapi/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/josephspurrier%2Fgowebapi/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/josephspurrier%2Fgowebapi/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/josephspurrier","download_url":"https://codeload.github.com/josephspurrier/gowebapi/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/josephspurrier%2Fgowebapi/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28428936,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-14T16:38:47.836Z","status":"ssl_error","status_checked_at":"2026-01-14T16:34:59.695Z","response_time":107,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api","go","golang"],"created_at":"2026-01-14T17:47:25.618Z","updated_at":"2026-01-14T17:47:26.320Z","avatar_url":"https://github.com/josephspurrier.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# gowebapi\n\n[![Go Report Card](https://goreportcard.com/badge/github.com/josephspurrier/gowebapi)](https://goreportcard.com/report/github.com/josephspurrier/gowebapi)\n[![Build Status](https://travis-ci.org/josephspurrier/gowebapi.svg)](https://travis-ci.org/josephspurrier/gowebapi)\n[![Coverage Status](https://coveralls.io/repos/github/josephspurrier/gowebapi/badge.svg?branch=master\u0026timestamp=20180718-01)](https://coveralls.io/github/josephspurrier/gowebapi?branch=master)\n\n[![Swagger Validator](http://online.swagger.io/validator?url=https://raw.githubusercontent.com/josephspurrier/gowebapi/master/src/app/webapi/swagger.json)](http://petstore.swagger.io/?url=https://raw.githubusercontent.com/josephspurrier/gowebapi/master/src/app/webapi/swagger.json)\n\n## Testable Web API in Go with Swagger\n\nThis 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.\n\n**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](https://github.com/josephspurrier/gowebapi/releases/tag/0.1-alpha). The current version is a significant refactor that follows better practices.\n\n**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.\n\nYou must use Go 1.7 or newer because this project uses the http context.\n\n## Quick Start with Docker Compose\n\nYou can build a docker image from this repository and set it up along with a MySQL container using docker compose.\n\n```bash\n# Create a docker image.\ndocker build -t webapi:latest .\n\n# Launch MySQL and the webapi with docker compose.\ndocker-compose up\n\n# Open your browser to http://localhost:8080 and you should be able to access the API.\n# Try using the Swagger spec:\n# http://petstore.swagger.io/?url=https://raw.githubusercontent.com/josephspurrier/gowebapi/master/src/app/webapi/swagger.json\n\n# Shutdown the containers.\ndocker-compose down\n```\n\n## Manual Start\n\nUse the following commands to start a MySQL container with Docker:\n\n```bash\n# Start MySQL without a password.\ndocker run -d --name=mysql57 -p 3306:3306 -e MYSQL_ALLOW_EMPTY_PASSWORD=yes mysql:5.7\n# or start MySQL with a password.\ndocker run -d --name=mysql57 -p 3306:3306 -e MYSQL_ROOT_PASSWORD=somepassword mysql:5.7\n\n# Create the database via docker exec.\ndocker exec mysql57 sh -c 'exec mysql -uroot -e \"CREATE DATABASE IF NOT EXISTS webapi DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci;\"'\n# Or create the database manually.\nCREATE DATABASE webapi DEFAULT CHARSET = utf8 COLLATE = utf8_unicode_ci;\n\n# CD to the CLI tool.\ncd src/app/webapi/cmd/cliapp\n\n# Build the CLI tool.\ngo build\n\n# Apply the database migrations without a password.\nDB_USERNAME=root DB_HOSTNAME=127.0.0.1 DB_PORT=3306 DB_DATABASE=webapi ./cliapp migrate all ../../../../../migration/mysql-v0.sql\n# or apply the database migrations with a password.\nDB_USERNAME=root DB_PASSWORD=somepassword DB_HOSTNAME=127.0.0.1 DB_PORT=3306 DB_DATABASE=webapi ./cliapp migrate all ../../../../../migration/mysql-v0.sql\n```\n\nUsing the database connection information above, follow the steps to set up the `config.json` file:\n\n```bash\n# Copy the config.json from the root of the project to the CLI app folder.\ncp config.json src/app/webapi/cmd/webapi/config.json\n\n# Edit the `Database` section so the connection information matches your MySQL instance.\n# The database password is read from the `config.json` file, but is overwritten by the environment variable, `DB_PASSWORD`, if it is set.\n\n# Generate a base64 encoded secret.\n./cliapp generate\n\n# Use the encoded secret above to replace the `JWT.Secret` value in the config.\n```\n\nNow you can start the API.\n\n```bash\n# CD to the webapi app folder.\ncd src/app/webapi/cmd/webapi\n\n# Build the app.\ngo build\n\n# Run the app.\n./webapi\n\n# Open your browser to this URL to see the **welcome** message and status **OK**: http://localhost:8080/v1\n```\n\nTo interact with the API, open your favorite REST client.\n\nYou'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.\n\nCurrently, only a Content-Type of `application/x-www-form-urlencoded` is supported when sending to the API.\n\n## Available Endpoints\n\nThe following endpoints are available:\n\n```\n* POST   /v1/user           - Create a new user\n* GET\t /v1/user/{user_id} - Retrieve a user by ID\n* GET\t /v1/user           - Retrieve a list of all users\n* PUT\t /v1/user/{user_id} - Update a user by ID\n* DELETE /v1/user/{user_id} - Delete a user by ID\n* DELETE /v1/user           - Delete all users\n```\n\n## Swagger\n\nThis projects uses [Swagger v2](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md) to document the API. The entire Swagger spec is generated from the code in this repository.\n\nThe Swagger UI linked back to this project can be viewed\n[here](http://petstore.swagger.io/?url=https://raw.githubusercontent.com/josephspurrier/gowebapi/master/src/app/webapi/swagger.json).\n\nThe Swagger spec JSON file is available\n[here](https://github.com/josephspurrier/gowebapi/blob/master/src/app/webapi/swagger.json).\n\n### Install Swagger\n\nThis 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.\n\n```bash\ngo get github.com/go-swagger/go-swagger/cmd/swagger\n```\n\n### Generate Swagger Spec\n\n```bash\n# CD to the webapi folder.\ncd src/app/webapi\n\n# Generate the swagger spec.\nswagger generate spec -o ./swagger.json\n\n# Replace 'example' with 'x-example' in the swagger spec.\n## MacOS\nsed -i '' -e 's/example/x\\-example/' ./swagger.json\n## Linux\nsed -i'' -e 's/example/x\\-example/' ./swagger.json\n\n# Validate the swagger spec.\nswagger validate ./swagger.json\n\n# Serve the spec for the browser.\nswagger serve -F=swagger ./swagger.json\n```\n\n## Dredd\n\nThis projects uses [Dredd](https://github.com/apiaryio/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.\n\nThe Go documentation for Dredd is [here](https://dredd.readthedocs.io/en/latest/hooks-go.html).\n\nSample output from Dredd is [here](https://github.com/josephspurrier/gowebapi/wiki/Dredd-Sample-Output).\n\n### Install Dredd\n\nYou must have MySQL running for these tests to pass.\n\n```bash\n# Install dredd.\nnpm install -g dredd\n\n# Get the goodman package for Go hooks.\ngo get github.com/snikch/goodman/cmd/goodman\n\n# CD to the webapi folder.\ncd src/app/webapi\n\n# Copy the testdata/config.json to the current directory.\ncp testdata/config.json ./config.json\n\n# Build the hooks app to load the test data.\ngo build -o ./cmd/hooks/hooks app/webapi/cmd/hooks\n\n# Start MySQL without a password.\ndocker run -d --name=mysql57 -p 3306:3306 -e MYSQL_ALLOW_EMPTY_PASSWORD=yes mysql:5.7\n\n# Run a test with dredd.\ndredd\n```\n\n## Vendoring\n\nThis project uses [dep](https://github.com/golang/dep). The `dep init` command was run from inside the `src/app/webapi` folder.\n\nThese packages are used in the project:\n- MySQL Driver: [github.com/go-sql-driver/mysql](https://github.com/go-sql-driver/mysql)\n- SQL to Struct: [github.com/jmoiron/sqlx](https://github.com/jmoiron/sqlx)\n- Routing: [github.com/matryer/way](https://github.com/matryer/way)\n- Request Validation: [github.com/go-playground/validator](https://github.com/go-playground/validator)\n- JSON Web Tokens (JWT): [github.com/dgrijalva/jwt-go](https://github.com/dgrijalva/jwt-go)\n- CLI and Flag Parser: [gopkg.in/alecthomas/kingpin.v2](https://gopkg.in/alecthomas/kingpin.v2)\n- Password Hashing: [golang.org/x/crypto/bcrypt](https://golang.org/x/crypto/bcrypt)\n\n## Folder Structure\n\nAll 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).\n\nIn the `src/app/webapi` folder, you see a few top level folders:\n- **cmd** - contains the main function and a static folder for the favicon.\n- **component** - contains sets of related endpoints and database code.\n- **internal** - contains project specific packages with dependencies.\n- **middleware** - contains http wrappers for logging and CORS.\n- **model** - contains the files with JSON structs that will outputted by the API.\n- **pkg** - contains generic packages without project specific dependencies - these can be safely moved to other projects without internal dependencies.\n- **store** - contains the files with SQL used to query the database.\n\n## Components\n\nIn the root of the `src/app/webapi/component` folder, you see:\n- **core.go** - contains the dependencies shared by all the components: logger, database connection, request bind/validation, and the responses.\n- **core_mock.go** - contains the mocked dependencies which can be used by tests to modify the mocked dependencies.\n- **interface.go** - contains all the interfaces for the dependencies so you can easily mock out each one for testing purposes.\n\nInside 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.\n\n## Store\n\nIn 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:\n- FindOneByID(dest query.IRecord, ID string) (found bool, err error)\n- FindAll(dest query.IRecord) (total int, err error)\n- ExistsByID(db query.IRecord, s string) (found bool, err error)\n- ExistsByField(db query.IRecord, field string, value string) (found bool, ID string, err error)\n- DeleteOneByID(dest query.IRecord, ID string) (affected int, err error)\n- DeleteAll(dest query.IRecord) (affected int, err error)\n\nThis 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.\n\nFor instance, to retrieve a single user from the database, you would use this code:\n\n```go\n// Create the store.\nu := store.NewUser(p.DB, p.Q)\n\n// Get a user.\nexists, err := u.FindOneByID(u, req.UserID)\n```\n\nThe code for the generic `FindOneByID()` is in the `pkg/query/query.go` file:\n\n```go\n// FindOneByID will find a record by string ID.\nfunc (q *Q) FindOneByID(dest IRecord, ID string) (exists bool, err error) {\n\terr = q.db.Get(dest, fmt.Sprintf(`\n\t\tSELECT * FROM %s\n\t\tWHERE %s = ?\n\t\tLIMIT 1`, dest.Table(), dest.PrimaryKey()),\n\t\tID)\n\treturn recordExists(err)\n}\n```\n\nIf 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:\n\n```go\n// FindOneByID will find a record by string ID excluding deleted records.\nfunc (x *User) FindOneByID(dest query.IRecord, ID string) (exists bool, err error) {\n\terr = x.db.Get(dest, fmt.Sprintf(`\n\t\tSELECT * FROM %s\n\t\tWHERE %s = ?\n\t\tAND deleted_at  IS NULL\n\t\tLIMIT 1`, dest.Table(), dest.PrimaryKey()),\n\t\tID)\n\n\tif err == nil {\n\t\treturn true, nil\n\t} else if err == sql.ErrNoRows {\n\t\treturn false, nil\n\t}\n\treturn false, err\n}\n```\n\nThis allows you to standardize on how to interact with your database models throughout the team.\n\n## Endpoint HTTP Handlers\n\nIn 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:\n\n```go\n// Routes will set up the endpoints.\nfunc (p *Endpoint) Routes(router component.IRouter) {\n\trouter.Post(\"/v1/user\", p.Create)\n\trouter.Get(\"/v1/user/:user_id\", p.Show)\n\trouter.Get(\"/v1/user\", p.Index)\n\trouter.Put(\"/v1/user/:user_id\", p.Update)\n\trouter.Delete(\"/v1/user/:user_id\", p.Destroy)\n\trouter.Delete(\"/v1/user\", p.DestroyAll)\n}\n```\n\nThe endpoints are separated into files under each component folder and they look like this:\n\n```go\nfunc (p *Endpoint) DestroyAll(w http.ResponseWriter, r *http.Request) (int, error) {\n\t// Create the store.\n\tu := store.NewUser(p.DB, p.Q)\n\n\t// Delete all items.\n\tcount, err := u.DeleteAll(u)\n\tif err != nil {\n\t\treturn http.StatusInternalServerError, err\n\t} else if count \u003c 1 {\n\t\treturn http.StatusBadRequest, errors.New(\"no users to delete\")\n\t}\n\n\treturn p.Response.OK(w, \"users deleted\")\n}\n```\n\n## Request Validation\n\nThe `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:\n\n```go\n// swagger:parameters UserCreate\ntype request struct {\n\t// in: formData\n\t// Required: true\n\tFirstName string `json:\"first_name\" validate:\"required\"`\n\t// in: formData\n\t// Required: true\n\tLastName string `json:\"last_name\" validate:\"required\"`\n\t// in: formData\n\t// Required: true\n\tEmail string `json:\"email\" validate:\"required,email\"`\n\t// in: formData\n\t// Required: true\n\tPassword string `json:\"password\" validate:\"required\"`\n}\n\n// Request validation.\nreq := new(request)\nif err := p.Bind.FormUnmarshal(req, r); err != nil {\n\treturn http.StatusBadRequest, err\n} else if err = p.Bind.Validate(req); err != nil {\n\treturn http.StatusBadRequest, err\n}\n```\n\n## Reflection\n\nThe `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.\n\n```go\nfunc (p *Endpoint) Index(w http.ResponseWriter, r *http.Request) (int, error) {\n\t// Create the DB store.\n\tu := store.NewUser(p.DB, p.Q)\n\n\t// Get all items.\n\tresults := make(store.UserGroup, 0)\n\t_, err := u.FindAll(\u0026results)\n\tif err != nil {\n\t\treturn http.StatusInternalServerError, err\n\t}\n\n\t// Copy the items to the JSON model.\n\tarr := make([]model.UserIndexResponseData, 0)\n\tfor _, u := range results {\n\t\titem := new(model.UserIndexResponseData)\n\t\terr = structcopy.ByTag(\u0026u, \"db\", item, \"json\")\n\t\tif err != nil {\n\t\t\treturn http.StatusInternalServerError, err\n\t\t}\n\t\tarr = append(arr, *item)\n\t}\n\n\t// Send the response.\n\tresp := new(model.UserIndexResponse)\n\tresp.Body.Status = http.StatusText(http.StatusOK)\n\tresp.Body.Data = arr\n\treturn p.Response.JSON(w, resp.Body)\n}\n```\n\n## Logging\n\nYou can disable logging on the server by setting an environment variable: `WEBAPI_LOG_LEVEL=none`\n\n## Testing\n\nAll the tests use a database called: `webapitest`. The quickest way to get it set up is:\n\n```bash\n# Launch MySQL in docker container.\ndocker run -d --name=mysql57 -p 3306:3306 -e MYSQL_ALLOW_EMPTY_PASSWORD=yes mysql:5.7\n\n# Create the database via docker exec.\ndocker exec mysql57 sh -c 'exec mysql -uroot -e \"CREATE DATABASE IF NOT EXISTS webapitest DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci;\"'\n\n# Or create the database manually.\nCREATE DATABASE webapitest DEFAULT CHARSET = utf8 COLLATE = utf8_unicode_ci;\n```\n\nYou can use these commands to run tests:\n\n```bash\n# CD to the folder.\ncd src/app/webapi\n\n# Test all the packages.\ngo test ./...\n\n# Get coverage of all tests.\ngo test -coverpkg=all ./...\n\n# Get the coverage map of the current folder.\ngo test -coverprofile cover.out \u0026\u0026 go tool cover -html=cover.out -o cover.html \u0026\u0026 open cover.html \u0026\u0026 sleep 5 \u0026\u0026 rm cover.html \u0026\u0026 rm cover.out\n\n# Get the coverage map of all the packages.\ngo test -coverprofile cover.out ./... \u0026\u0026 go tool cover -html=cover.out -o cover.html \u0026\u0026 open cover.html \u0026\u0026 sleep 5 \u0026\u0026 rm cover.html \u0026\u0026 rm cover.out\n\n# Get the total code coverage - this only takes into consideration packages that\n# have a test file in them.\ngo test ./... -coverprofile cover.out; go tool cover -func cover.out\n```\n\n## Conventions\n\nRules for mapping HTTP methods to CRUD:\n\n```\nPOST   - Create (add record into database)\nGET    - Read (get record from the database)\nPUT    - Update (edit record in the database)\nDELETE - Delete (remove record from the database)\n```\n\nRules for HTTP status codes:\n\n```\n* Create something            - 201 (Created)\n* Read something              - 200 (OK)\n* Update something            - 200 (OK)\n* Delete something            - 200 (OK)\n* Missing request information - 400 (Bad Request)\n* Unauthorized operation      - 401 (Unauthorized)\n* Any other error             - 500 (Internal Server Error)\n```\n\n## Resources\n\nThese are all good reads:\n\nCustom HTTP Handlers:\n\n- https://blog.golang.org/error-handling-and-go\n- https://mwholt.blogspot.com/2015/05/handling-errors-in-http-handlers-in-go.html\n- https://medium.com/statuscode/how-i-write-go-http-services-after-seven-years-37c208122831\n\nPackage Layout:\n\n- https://medium.com/@benbjohnson/standard-package-layout-7cdbc8391fc1\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjosephspurrier%2Fgowebapi","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjosephspurrier%2Fgowebapi","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjosephspurrier%2Fgowebapi/lists"}