reply is a Go library that supports developers with shaping and standardising the responses sent from their API service(s). It also allows users to predefine non-successful error objects, giving a granularity down to title, description, status code, and many more through the error manifest(s) passed to the Replier.
- Installation
- Getting Started
- How to send a response(s)
- Transfer Objects
- Response Types
- Copyright
go get github.com/ooaklee/replyThere are several ways you can integrate reply into your application. Below, you will find an example of how you can get the most out of this package.
When creating a Replier, you only have to pass a reply.ErrorManifest collection. The collection can be empty or contain as many entries as you'd like.
Just remember, when creating an Error Response (Multi or Single), the passed manifest will be used.
// Have a definition of errors you want to use in your application
var (
// errExample404 is an example 404 error
errExample404 = errors.New("example-404-error")
// errExampleDobValidation is an example dob validation error
errExampleDobValidation = errors.New("example-dob-validation-error")
// errExampleNameValidation is an example name validation error
errExampleNameValidation = errors.New("example-name-validation-error")
// errExampleMissing is an example missing error
errExampleMissing = errors.New("example-missing-error")
// errExampleEmailValidation is an example email validation error
errExampleEmailValidation = errors.New("example-email-validation-error")
)
// (Optional) Create an error manifest to hold correlating errors and their manifest item.
// These can be also be sourced from relevant packages to populate
//
// See how we have to reply.ErrorManifests, on with mulitple
// items and the other with just one.
baseManifest := []reply.ErrorManifest{
{
errExample404: reply.ErrorManifestItem{Title: "resource not found", StatusCode: http.StatusNotFound},
errExampleNameValidation: reply.ErrorManifestItem{Title: "Validation Error", Detail: "The name provided does not meet validation requirements", StatusCode: http.StatusBadRequest, About: "www.example.com/reply/validation/1011", Code: "1011"},
},
{errExampleDobValidation: reply.ErrorManifestItem{Title: "Validation Error", Detail: "Check your DoB, and try again.", Code: "100YT", StatusCode: http.StatusBadRequest}},
// example using error from another package
// {somepackage.ErrNotFound: reply.ErrorManifestItem{Title: "Not Found", Detail: "The requested resource was not found", Code: "404", StatusCode: http.StatusNotFound}},
}
// Create Replier to manage the responses going back to consumer(s)
replier := reply.NewReplier(baseManifest)NOTE - By default, if an
Error Manifest Itemdoes not have aStatusCodeset,replywill default to400 (Bad Request).
The ErrorManifest contains an error and its corresponding ErrorManifestItem.
The ErrorManifestItem is used to explicitly define the attributes to include in your response's error object. Like previously mentioned, the ErrorManifestItem should be created against the explicit error used in your code or taken from a package you're using.
ErrorManifestItems will come in various sizes depending on how much information you what to make visible to your consumer.
It is essential to evaluate the exposure level of your API continuously. Is it something that will be used external to your team/ business, and thus minimal information should be given?
The key attributes of the ErrorManifestItem are:
-
Title (
string): Summary of the error being returned. Try keeping it short and sweet. -
Detail (
string): Gives a more descriptive outline of the error, something with more context. -
StatusCode (
int): The HTTP Status Code associated with respective error. If it's a5XXerror, it will be the sole error object returned in amulti error responsescenario. -
About (
string): The URL to a page that gives more context about the error -
Code (
string): Theinternal code(application or business) that's used to identify the error -
Meta (
interface{}): Any additional meta-information you may want to pass with your error object
Assuming an ErrorManifest containing the following entry was passed to a Replier,
// manifest item references error defined in previous example
{errExampleNameValidation: reply.ErrorManifestItem{Title: "Validation Error", Detail: "The name provided does not meet validation requirements", StatusCode: http.StatusBadRequest, About: "www.example.com/reply/validation/1011", Code: "1011"}}And its respective error was passed when creating a new error response (NewHTTPErrorResponse). reply would return the following JSON response:
{
"errors": [
{
"title": "Validation Error",
"detail": "The name provided does not meet validation requirements",
"about": "www.example.com/reply/validation/1011",
"status": "400",
"code": "1011"
}
]
}If instead, multiple errors were passed to the NewHTTPMultiErrorResponse method or errors keys were wrapped and passed to NewHTTPErrorResponse, and all had an entry in the ErrorManifest, reply would return a response similar to the following JSON response:
{
"errors": [
{
"title": "Validation Error",
"detail": "The name provided does not meet validation requirements",
"about": "www.example.com/reply/validation/1011",
"status": "400",
"code": "1011"
},
{
"title": "Validation Error",
"detail": "The email provided does not meet validation requirements",
"status": "400"
}
]
}NOTE - Not all attributes in the
ErrorManifestItemhave to be specified. By default, if aStatusCodeis not provided in the item400would be set.
NOTE - You can create your own custom error json response shape, by using the
reply.WithTransferObjectErroroption when creating your replier. Check the **example simple api ** implementation (replierWithCustomTransitionObjs) for a working example.
At the core, you can use reply two send both successful and error responses.
When sending an error response, it is essential to make sure you populate the Error Manifest passed to the Replier with the correct errors source from your code or packages. Otherwise, a 500 - Internal Server Error response will be sent back to the client by default if it cannot match the passed error in the manifest.
When matching
errors.Isis used, the error in the manifest should be the same error as the one passed.
Having expected ErrorManifest entries are especially important for Multi Error responses. One unmatched error will return a single 500 - Internal Server Error instead of the array of passed error responses.
There are currently 3 Replier methods that make use of the Error Manifest. These methods are NewHTTPResponse, NewHTTPMultiErrorResponse and NewHTTPErrorResponse.
NOTE -
NewHTTPResponseis the base of both theNewHTTPMultiErrorResponseandNewHTTPErrorResponseaides.NOTE - You can get
NewHTTPErrorResponseto behave likeNewHTTPMultiErrorResponseby wrapping your error keys witherrors.Join(errs...)
Below you will find an example using NewHTTPResponse. However, for simplicity, it's recommended you use one of the error aides. The error aide implementation is outlined HERE.
// ExampleHandler handler to demostrate how to use package for error
// response
func ExampleHandler(w http.ResponseWriter, r *http.Request) {
// Pass error to Replier's method to return predefined response, else
// 500
_ = replier.NewHTTPResponse(&reply.NewResponseRequest{
Writer: w,
// errExample404 defined in previous example
Error: errExample404,
})
}When the endpoint linked to the handler above is called, you should see the following JSON response.
{
"errors": [
{
"title": "resource not found",
"status": "404"
}
]
}
NOTE- ThebaseManifestwas initially declared, and its item represents the response shown below. The status code is both shown in the response body as a string, and it is also set accordingly.
The 3 Replier methods that can send "successful responses" are NewHTTPResponse, NewHTTPBlankResponse and NewHTTPDataResponse.
NOTE -
NewHTTPResponseis the base of both theNewHTTPBlankResponseandNewHTTPDataResponseaides.
Below you will find an example using NewHTTPResponse, however for simplicity, it's recommended you use either of the follow aide implementations:
// ExampleGetAllHandler handler to demostrate how to use package for successful
// response
func ExampleGetAllHandler(w http.ResponseWriter, r *http.Request) {
// building sample user model
type user struct {
ID int `json:"id"`
Name string `json:"name"`
}
// emulate users pulled from repository
mockedQueriedUsers := []user{
{ID: 1, Name: "John Doe"},
{ID: 2, Name: "Sam Smith"},
}
// build and sent default formatted JSON response for consumption
// by client
_ = replier.NewHTTPResponse(&reply.NewResponseRequest{
Writer: w,
Data: mockedUsers,
StatusCode: htttp.StatusOK,
})
}When the endpoint linked to the handler above is called, you should see the following JSON response.
{
"data": [
{
"id": 1,
"name": "John Doe"
},
{
"id": 2,
"name": "Sam Smith"
}
]
}
NOTE- Unlike the error use case, successful requests expect theStatusCodeto be defined when creating a successful response. If you do not provide a status code, 200 will be assumed.It is recommend to use use either the Blank Response Aide or Data Response Aide based on your desired ouput
Transfer objects are used to define the shape of various elements within the overall response. In particular, they are used for the base response object and the individual error response object.
If desired, users can create their own transfer object for the base and individual error response objects with additional logic.
The Transfer Object used for the base response object must satisfy the following interface:
// TransferObject outlines expected methods of a transfer object
type TransferObject interface {
SetHeaders(headers map[string]string)
SetStatusCode(code int)
SetMeta(meta map[string]interface{})
SetTokenOne(token string)
SetTokenTwo(token string)
GetWriter() http.ResponseWriter
GetStatusCode() int
SetWriter(writer http.ResponseWriter)
SetStatus(transferObjectStatus *TransferObjectStatus)
RefreshTransferObject() TransferObject
SetData(data interface{})
}The interface uses relatively self-explanatory method names. Still, if you want to see an example of how one might create your own transfer object, you can find the default transfer object used by reply here (defaultReplyTransferObject).
Once your transfer object has been created and is valid, you can overwrite the default transfer object in your newly created version by using the following code when declaring your Replier:
// some implementation of your desired transfer object
var customTransferObject reply.TransferObject
customTransferObject = &foo{}
// create a Replier, overwriting the default transfer object
replier := reply.NewReplier([]reply.ErrorManifest{}, reply.WithTransferObject(customTransferObject))
// use the new Replier as you otherwise wouldNOTE: you can also pass in your custom transfer object with
&foo{}, for example:
replier := reply.NewReplier([]reply.ErrorManifest{}, reply.WithTransferObject(&foo{}))
For a live example on how you can use a custom
transfer object, please look at thesimple API examplesin this repo. You are looking out for thefooReplyTransferObjectimplementation.
The Transfer Object used for the individual error response object must satisfy the following interface:
// TransferObjectError outlines expected methods of a transfer object error
type TransferObjectError interface {
SetTitle(title string)
GetTitle() string
SetDetail(detail string)
GetDetail() string
SetAbout(about string)
GetAbout() string
SetStatusCode(status int)
GetStatusCode() string
SetCode(code string)
GetCode() string
SetMeta(meta interface{})
GetMeta() interface{}
RefreshTransferObject() TransferObjectError
}The interface uses relatively self-explanatory method names. Look at the deeper dive of the Error Manifest Item to better understand how these methods are used.
Suppose you want to see an example of how one might create your own transfer object error. In that case, you can find the default transfer object error used by reply here (defaultReplyTransferObjectError).
You can overwrite the default transfer object error used by your Replier by using the following code when declaring your Replier:
// some implementation of your desired transfer object
var customTransferObjectError reply.TransferObjectError
customTransferObjectError = &bar{}
// create a Replier, overwriting the default transfer object error (error transfer object)
replier := reply.NewReplier([]reply.ErrorManifest{}, reply.WithTransferObjectError(customTransferObjectError))
// use the new Replier as you otherwise wouldNOTE: you can also pass in your custom transfer object with
&bar{}, for example:
replier := reply.NewReplier([]reply.ErrorManifest{}, reply.customTransferObjectError(&bar{}))
For a live example on how you can use a custom
transfer object errorin combination with a customtransfer object, please look at thesimple API examplesin this repo. You are looking out for thereplierWithCustomTransitionObjsimplementation.You can set your custom
transfer object errorindividually, as shown above.
There are currently four core response types supported by reply. They are the Error, Token, Data and Default response types. Each type has its JSON representation defined through a Transfer Object.
NOTE: Unless otherwise stated, the
Transfer Objectsassumed will be the default transfer object (defaultReplyTransferObject) and default transfer object error (defaultReplyTransferObjectError).
All core response types share universal attributes, which you can set in addition to their outputs. These include:
- Headers
- Meta
- Status Code
NOTE -
Status Codeis set at different levels dependant onresponse type. For example, theerror response typeis handled in theErrorManifest.
The Error response notifies the consumer when an error/ unexpected behaviour has occurred on the API. There are 2 types of Error Response Types, NewHTTPErrorResponse and NewHTTPMultiErrorResponse.
Where
NewHTTPMultiErrorResponseexplicitly expects a slice of errors,NewHTTPErrorResponsecan also return multi errors response by wrapping the manifest errors witherrors.Join(errs...).
The error response object forwarded to the consumer is sourced from the error manifest. In the event the error's string
representation isn't in the manifest; reply will return the consumer a "500 - Internal Server Error" response.
To create an individual error response use the following code snippet:
// create error manifest using errors defined in application (see previous example for error definitions)
baseManifest := []reply.ErrorManifest{
{errExample404: reply.ErrorManifestItem{Title: "resource not found", StatusCode: http.StatusNotFound},
errExampleNameValidation: reply.ErrorManifestItem{Title: "Validation Error", Detail: "The name provided does not meet validation requirements", StatusCode: http.StatusBadRequest, About: "www.example.com/reply/validation/1011", Code: "1011"},
},
errExampleDobValidation: reply.ErrorManifestItem{Title: "Validation Error", Detail: "Check your DoB, and try again.", Code: "100YT", StatusCode: http.StatusBadRequest},
}
// create Replier based on error manifest
replier := reply.NewReplier(baseManifest)
func ExampleHandler(w http.ResponseWriter, r *http.Request) {
_ = replier.NewHTTPResponse(&reply.NewResponseRequest{
Writer: w,
Error: errExample404,
})
}If you wanted to send a multi error response, you could use the following, assuming the same Replier from above is being used:
func ExampleHandler(w http.ResponseWriter, r *http.Request) {
// errors returned
exampleErrs := []errors{
errExampleNameValidation,
errExampleDobValidation,
}
_ = replier.NewHTTPResponse(&reply.NewResponseRequest{
Writer: w,
Errors: exampleErrs,
})
}You can also send a multi error response with a single error by wrapping the error keys with errors.Join(errs...):
func ExampleHandler(w http.ResponseWriter, r *http.Request) {
// errors returned
exampleWrappedErr := errors.Join(errExampleNameValidation, errExampleEmailValidation)
_ = replier.NewHTTPResponse(&reply.NewResponseRequest{
Writer: w,
Error: exampleWrappedErr,
})
}For readability and simplicity, you can use the HTTP error response aides. You can find code snippets using these aides below:
Individual Error
// inside of the request handler
_ = replier.NewHTTPErrorResponse(w, exampleErr)Multi Error
// inside of the request handler
_ = replier.NewHTTPMultiErrorResponse(w, exampleErrs)You can also add additional headers and meta data to the response by using the optional WithHeaders and/ or WithMeta response attributes respectively. For example:
_ = replier.NewHTTPErrorResponse(w, exampleErr, reply.WithMeta(map[string]interface{}{
"example": "meta in error reponse",
}))OR
_ = replier.NewHTTPMultiErrorResponse(w, exampleErrs, reply.WithMeta(map[string]interface{}{
"example": "meta in error reponse",
}))Error responses are returned with the format. The following responses are based on the examples above, so your response content will vary.
Individual Error
{
"errors": [
{
"title": "resource not found",
"status": "404"
}
]
}Multi Error
{
"errors": [
{
"title": "Validation Error",
"detail": "The name provided does not meet validation requirements",
"about": "www.example.com/reply/validation/1011",
"status": "400",
"code": "1011"
},
{
"title": "Validation Error",
"detail": "The email provided does not meetvalidation requirements",
"status": "400"
}
]
}When a meta is also declared, the response will have the following format. It can be as big or small as needed.
Individual Error
{
"errors": [
{
"title": "resource not found",
"status": "404"
}
],
"meta": {
"example": "meta in error reponse"
}
}Multi Error
{
"errors": [
{
"title": "Validation Error",
"detail": "The name provided does not meet validation requirements",
"about": "www.example.com/reply/validation/1011",
"status": "400",
"code": "1011"
},
{
"title": "Validation Error",
"detail": "The email provided does not meetvalidation requirements",
"status": "400"
}
],
"meta": {
"example": "meta in error reponse"
}
}The token response sends the consumer tokens. Currently, it is limited to 2 tokens, and with the default Transfer Object, TokenOne represents access_token, and TokenTwo represents refresh_token. However, if you use other ID/ JSON attributes to describe your tokens for your API, you can create a Custom Transfer Object.
Again, when using the default Transfer Object, the supported TokenOne and TokenTwo represent acccess_token and refresh_token, respectively. If either is passed in the response request, reply will default to this response type.
To create a token response use the following code snippet:
replier := reply.NewReplier([]reply.ErrorManifest{})
func ExampleHandler(w http.ResponseWriter, r *http.Request) {
// do something to get tokens
_ = replier.NewHTTPResponse(&reply.NewResponseRequest{
Writer: w,
TokenOne: "08a0a043-b532-4cea-8117-364739f2d994",
TokenTwo: "08b29914-09a8-4a4a-8aa5-b1ffaff266e6",
StatusCode: 200,
})
}For readability and simplicity, you can use the HTTP token response aide. You can find a code snippet using this aide below:
// inside of the request handler
_ = replier.NewHTTPTokenResponse(w, 200, "08a0a043-b532-4cea-8117-364739f2d994", "08b29914-09a8-4a4a-8aa5-b1ffaff266e6")You can also add additional headers and meta data to the response by using the optional WithHeaders and/ or WithMeta response attributes respectively. For example:
_ = replier.NewHTTPTokenResponse(w, 200, "08a0a043-b532-4cea-8117-364739f2d994", "08b29914-09a8-4a4a-8aa5-b1ffaff266e6", reply.WithMeta(map[string]interface{}{
"example": "meta in token reponse",
}))NOTE: If you only want to return one token, pass an empty string, i.e.
"". Although, you must give at least one token string.
Error responses are returned with the format.
{
"access_token": "08a0a043-b532-4cea-8117-364739f2d994",
"refresh_token": "08b29914-09a8-4a4a-8aa5-b1ffaff266e6"
}When a meta is also declared, the response will have the following format. It can be as big or small as needed.
{
"access_token": "08a0a043-b532-4cea-8117-364739f2d994",
"refresh_token": "08b29914-09a8-4a4a-8aa5-b1ffaff266e6",
"meta": {
"example": "meta in token reponse"
}
}The data response can be seen as a successful response. It parses the passed struct into its JSON representation and passes it to the consumer in the JSON response. The JSON response below will represent a response if the data passed was a user struct with the:
id1namejohn doedob1/1/1970
To create a data response use the following code snippet:
type user struct {
id int `json:"id"`
name string `json:"name"`
dob string `json:"dob"`
}
replier := reply.NewReplier([]reply.ErrorManifest{})
func ExampleHandler(w http.ResponseWriter, r *http.Request) {
u := user{
id: 1,
name: "john doe",
dob: "1/1/1970",
}
_ = replier.NewHTTPResponse(&reply.NewResponseRequest{
Writer: w,
Data: u,
StatusCode: 201,
})
}For readability and simplicity, you can use the HTTP data (successful) response aide. You can find a code snippet using this aide below:
// inside of the request handler
_ = replier.NewHTTPDataResponse(w, 201, u)You can also add additional headers and meta data to the response by using the optional WithHeaders and/ or WithMeta response attributes respectively. For example:
_ = replier.NewHTTPDataResponse(w, 201, u, reply.WithMeta(map[string]interface{}{
"example": "meta in data reponse",
}))Data responses are returned with the format.
{
"data": {
"id": 1,
"name": "john doe",
"dob": "1/1/1970"
}
}When a meta is also declared, the response will have the following format. It can be as big or small as needed.
{
"data": {
"id": 1,
"name": "john doe",
"dob": "1/1/1970"
},
"meta": {
"example": "meta in data reponse"
}
}The default (blank) response returns "{}" with a status code of 200 if no error, tokens, data and status code is passed. If desired, another status code can be specified with default responses.
To create a default response use the following code snippet:
replier := reply.NewReplier([]reply.ErrorManifest{})
func ExampleHandler(w http.ResponseWriter, r *http.Request) {
_ = replier.NewHTTPResponse(&reply.NewResponseRequest{
Writer: w,
StatusCode: 200,
})
}For readability and simplicity, you can use the HTTP default (blank) response aide. You can find a code snippet using this aide below:
// inside of the request handler
_ = replier.NewHTTPBlankResponse(w, 200)You can also add additional headers and meta data to the response by using the optional WithHeaders and/ or WithMeta response attributes respectively. For example:
_ = replier.NewHTTPBlankResponse(w, 200, reply.WithMeta(map[string]interface{}{
"example": "meta in default reponse",
}))Default responses are returned with the format.
{
"data": "{}"
}When a meta is also declared, the response will have the following format. It can be as big or small as needed.
{
"data": "{}",
"meta": {
"example": "meta in default reponse"
}
}Copyright (C) 2021 by Leon Silcott leon@boasi.io.
reply library released under MIT License. See LICENSE for details.