There are several best practices when it comes to configuring your server:
- separate the handler from the framework, this allows you to change routers
- add graceful shutdown
- set read timeouts
- set write timeouts
- handle context cancellation
- limit the size of the request payload and headers
- add rate-limiting for routes
- structure the folders based on the API routes
- e.g. /health -> /rest/api/health_controller.go
- e.g. /v1/products -> /rest/api/v1/product_controller.go
- middlewares, context, and error
- serialization for response (data envelope)
- copy the body, so that you can retrieve it later for logging purposes
- map domain errors to http status errors
- document the environment variables in the
.env.sample
Base Endpoint
Each versioned endpoint will have an API
struct. The root /
endpoint API
struct can be found in rest/api/api.go
:
Lines 8 to 12 in 9e8f4d9
Here, we register the resource controllers as well as middlewares for the endpoint.
Adding Routes
Each API
struct will have a Register
method where we will register the resource routes.
Lines 14 to 24 in 9e8f4d9
What are Controllers
Controllers are a collection of resources. Each controller can have several methods that maps to the HTTP methods.
Adding new API
This example demonstrates on how to add a new API endpoint
Goal: Add a new
GET /v1/products
endpoint
- Go to
rest/api/v1
folder - Create a new file
product_controller.go
- Create a new struct
ProductController
- Create a constructor
NewProductController
- Add a method
List
package v1
type ProductController struct {
productUC ProductUsecase
}
func (h *ProductController) List(w http.ResponseWriter, r *http.Request) {
p, err := h.productUC.List(r.Context())
if err != nil {
response.JSONError(w, err)
return
}
response.JSON(w, response.OK(&p), http.StatusOK)
}
- Go to
rest/api/v1.go
- Add the
ProductController
to theAPI
struct - Mount the routes accordingly
package v1
type API struct {
*ProductController
}
func (api *API) Register(r chi.Router) {
r.Route("/v1", func(r chi.Router) {
r.Route("/products", func(r chi.Router) {
r.Get("/", api.ProductController.List)
})
})
}
- how to handle auth
- getting jwt claims
Guarding Routes
To guard routes, we can mount the RequireAuth
middleware.
- Go to
rest/api/v1.go
(or specific versioned endpoint) - Add the
RequireAuth
middleware to the structAPI
- Attach the
RequireAuth
to the routes that you want to protect in theRegister
method
package v1
import (
"github.com/alextanhongpin/core/http/middleware"
"github.com/go-chi/chi/v5"
)
type API struct {
RequireAuth middleware.Middleware
*CategoryController
}
func (api *API) Register(r chi.Router) {
r.Route("/v1", func(r chi.Router) {
r.Route("/categories", func(r chi.Router) {
// Attach to a single route
r.With(api.RequireAuth).Post("/", api.CategoryController.Create)
})
// Attach to a group
r.Group(func(r chi.Router) {
r.Use(api.RequireAuth)
})
})
}
- validation request
- request payload size
- body parser
- trim strings
- query filters
- url builders
- forms and file uploads
- response envelope, links, status code and error handling
- request id
- cors
- auth bearer/basic
- healthcheck
APP_VERSION=<optional: the current app version, e.g. 0.0.1>
JWT_SECRET=<required: provide a secret for jwt>
- first endpoint
- document endpoint
- first test
- conventions
- authorization
- whitelist ip
- webhooks (notifications, callbacks) handler, security and testing
- localization
- versioning
- dependency injection
- OTP flow
Call the register endpoint to generate the token.
# -r means raw output. We want the string without the json double quotes
# Sends the output to the clipboard.
$ curl -XPOST localhost:8080/register | jq -r .data.accessToken | pbcopy
Make a call to the protected endpoint using the token:
$ curl -XPOST -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2ODI1MjgxODMsInN1YiI6IjllZTNkZDI2LWY5MWItNDNjMy04NzJkLTJlNjg0YzBjOTIzYyJ9.GFZl5v0JXC72PpGa2953Ioh3xd7nM9ezI4YL-rYNK7Q' localhost:8080/v1/categories
What is the goal of testing the API? There are many different ways of testing too, such as using Postman, writing code etc.
For now, we stick with the following goals:
- tests as documentation guide
- tests as validation for behavior
- tests as a way to describe expected output json
- tests as a workflow guide
Test should serve as documentation. Tools like openAPI for example may show the sample expected request/response, but they don't show scenarios when you use different payload. For example, if you were to build a simple payment endpoint similar to Stripe, you will have different test cards that could trigger different scenarios. The requests are usually query string, path params, and body payload and http headers as well. The response we want to validate is usually the http headers as well as the payload body or error.
Test scenarios can be written in BDD style:
Given that User calls the POST /payments
When the card is invalid
Then the API will error with status 422
And User will see Error Card Rejected.
Some business flows are easier to capture programmatically too. APIs workflows can consists of different steps, such as initially authenticating the users, then populating the data to be queries etc, as well as chaining multiple api steps.
Should the API be making actual database calls or mutating data? probably not. we just want to simulate the request response.