Skip to content
/ reply Public

πŸ’¬ Go library to shape and standardise the responses sent by API services πŸ’¬

License

Notifications You must be signed in to change notification settings

ooaklee/reply

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

12 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

reply

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.

Table of Contents


Installation

  go get github.com/ooaklee/reply

Getting Started

There 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.

How to create a Replier

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.

// (Optional) Create an error manifest to hold correlating errors as a string and their manifest
// item
//
// See how we have to reply.ErrorManifests, on with mulitple
// items and the other with just one.
baseManifest := []reply.ErrorManifest{
    {
      "example-404-error": reply.ErrorManifestItem{Title: "resource not found", StatusCode: http.StatusNotFound},
      "example-name-validation-error": 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"},
    },
    {"example-dob-validation-error": reply.ErrorManifestItem{Title: "Validation Error", Detail: "Check your DoB, and try again.", Code: "100YT", StatusCode: http.StatusBadRequest}},
  }

// Create Replier to manage the responses going back to consumer(s)
replier := reply.NewReplier(baseManifest)

NOTE - By default, if an Error Manifest Item does not have a StatusCode set, reply will default to 400 (Bad Request).

More about the ErrorManifest

The ErrorManifest contains a string key which is the string representation of an error type 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 string key is returned when err.Error() is run, assuming err was the standard error type.

Deeper look into ErrorManifestItem

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 a 5XX error, it will be the sole error object returned in a multi error response scenario.

  • About (string): The URL to a page that gives more context about the error

  • Code (string): The internal 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,

{"example-name-validation-error": 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, 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 ErrorManifestItem have to be specified. By default, if a StatusCode is not provided in the item 400 would be set.

NOTE - You can create your own custom error json response shape, by using the reply.WithTransferObjectError option when creating your replier. Check the **example simple api ** implementation (replierWithCustomTransitionObjs) for a working example.

How to send a response(s)

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 error key strings. 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.

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.

Making use of error manifest

There are currently 3 Replier methods that make use of the Error Manifest. These methods are NewHTTPResponse, NewHTTPMultiErrorResponse and NewHTTPErrorResponse.

NOTE - NewHTTPResponse is the base of both the NewHTTPMultiErrorResponse and NewHTTPErrorResponse aides.

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) {

  // Create error with value corresponding to one of the manifest's entry's key
  exampleErr := errors.New("example-404-error")


  // Pass error to Replier's method to return predefined response, else
  // 500
  _ = replier.NewHTTPResponse(&reply.NewResponseRequest{
    Writer: w,
    Error:  exampleErr,
  })
}

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 - The baseManifest was 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.

Sending "successful responses"

The 3 Replier methods that can send "successful responses" are NewHTTPResponse, NewHTTPBlankResponse and NewHTTPDataResponse.

NOTE - NewHTTPResponse is the base of both the NewHTTPBlankResponse and NewHTTPDataResponse aides.

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 the StatusCode to 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

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.

Base Transfer Object (TransferObject)

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 would

NOTE: 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 the simple API examples in this repo. You are looking out for the fooReplyTransferObject implementation.

Error Transfer Object (TransferObjectError)

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 would

NOTE: 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 error in combination with a custom transfer object, please look at the simple API examples in this repo. You are looking out for the replierWithCustomTransitionObjs implementation.

You can set your custom transfer object error individually, as shown above.

Response Types

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 Objects assumed will be the default transfer object (defaultReplyTransferObject) and default transfer object error (defaultReplyTransferObjectError).

Universal Attributes

All core response types share universal attributes, which you can set in addition to their outputs. These include:

  • Headers
  • Meta
  • Status Code

NOTE - Status Code is set at different levels dependant on response type. For example, the error response type is handled in the ErrorManifest.

Error Response Type

The Error response notifies the consumer when an error/ unexpected behaviour has occurred on the API. There are 2 types of Error Response Types, Individual (NewHTTPErrorResponse) and Multi (NewHTTPMultiErrorResponse).

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.

As code (including aide example)

To create an individual error response use the following code snippet:

// create error manifest
baseManifest := []reply.ErrorManifest{
  {"example-404-error": reply.ErrorManifestItem{Title: "resource not found", StatusCode: http.StatusNotFound},
    "example-name-validation-error": 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"},
  },
  "example-dob-validation-error": 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) {

  // error returned
  exampleErr := errors.New("example-404-error")

  _ = replier.NewHTTPResponse(&reply.NewResponseRequest{
    Writer: w,
    Error:  exampleErr,
  })
}

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{
    errors.New("example-name-validation-error"),
    errors.New("example-dob-validation-error"),
  }

  _ = replier.NewHTTPResponse(&reply.NewResponseRequest{
    Writer: w,
    Errors: exampleErrs,
  })
}

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",
  }))

JSON Representation

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"
    }
  ]
}
With Meta

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"
  }
}

Token Response Type

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.

As code (including aide example)

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.

JSON Representation

Error responses are returned with the format.

{
  "access_token": "08a0a043-b532-4cea-8117-364739f2d994",
  "refresh_token": "08b29914-09a8-4a4a-8aa5-b1ffaff266e6"
}
With Meta

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"
  }
}

Data Response Type

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:

  • id 1
  • name john doe
  • dob 1/1/1970

As code (including aide example)

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",
  }))

JSON Representation

Data responses are returned with the format.

{
  "data": {
    "id": 1,
    "name": "john doe",
    "dob": "1/1/1970"
  }
}
With Meta

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"
  }
}

Default (Blank) Response Type

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.

As code (including aide example)

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",
  }))

JSON Representation

Default responses are returned with the format.

{
  "data": "{}"
}
With Meta

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

Copyright (C) 2021 by Leon Silcott leon@boasi.io.

reply library released under MIT License. See LICENSE for details.