How to define a generic success / error result for API responses?

I’m coming from TypeScript and want to define objects for my API responses. In TS I used

type Result<T> = { isSuccessful: true; value: T; } | { isSuccessful: false; code: string; message: string; };

In Go I started with

type ResponseResultError struct {
	Code    string `json:"code"`
	Message string `json:"message"`
}

type ResponseResult[T any] struct {
	Ok    bool                `json:"ok"`
	Value T                   `json:"value"`
	Error ResponseResultError `json:"error"`
}

but since Go supports tuples etc. maybe there are better solutions to solve this? How would you define “something” people can consume to either create a success response containing

{ isSuccessful: true; value: T; }

or an error response containing

{ isSuccessful: false; code: string; message: string; }

?


Or should I go for the following?

type InternalErrorResponseResult struct {
	IsSuccessful bool   `json:"isSuccessful"`
	Code         string `json:"code"`
	Message      string `json:"message"`
}

func NewInternalErrorResponseResult(code string, message string) InternalErrorResponseResult {
	return InternalErrorResponseResult{
		Code:    code,
		Message: message,
	}
}

type OperationResponseResult[T any] struct {
	IsSuccessful bool `json:"isSuccessful"`
	Value        T    `json:"value"`
}

func NewOperationResponseResult[T any](isSuccessful bool, value T) OperationResponseResult[T] {
	return OperationResponseResult[T]{
		IsSuccessful: isSuccessful,
		Value:        value,
	}
}

Your code looks fine to me. Is the rub that you only want one struct type? If so you should check out omitEmpty:

The “omitempty” option specifies that the field should be omitted from the encoding if the field has an empty value, defined as false, 0, a nil pointer, a nil interface value, and any empty array, slice, map, or string.

You could define your struct as follows:

type Result[T any] struct {
	IsSuccessful bool   `json:"isSuccessful"`
	Value        *T     `json:"value,omitempty"`
	Code         string `json:"code,omitempty"`
	Message      string `json:"message,omitempty"`
}

… to where it just doesn’t send the keys when they are zero values or nil. Since it’s idiomatic to use err != nil to determine whether something went wrong, you could also potentially simplify your constructor code to something like this if you wanted:

func NewResult[T any](value *T, err error) Result[T] {
	if err != nil {
		return Result[T]{
			IsSuccessful: false,
			Code:         http.StatusText(http.StatusInternalServerError),
			Message:      err.Error(),
		}
	}
	return Result[T]{
		IsSuccessful: true,
		Value:        value,
	}
}

Because again it’s idiomatic to do resp, err := doSomething() and it would flow to just pass resp, err into this NewResult func in my opinion. Put that together with some trite test code:

type someItem struct {
	ID    int
	Value string
}

func main() {
	value := NewResult(&someItem{23, "Hello"}, nil)
	json.NewEncoder(os.Stdout).Encode(value)
	errorValue := NewResult[someItem](nil, errors.New("Problem contacting database."))
	json.NewEncoder(os.Stdout).Encode(errorValue)
}

… will yield:

{"isSuccessful":true,"value":{"ID":23,"Value":"Hello"}}
{"isSuccessful":false,"code":"Internal Server Error","message":"Problem contacting database."}

Anyway, just some food for thought! You could also push this to a function like SendJSON[T any](value *T, err error, w http.ResponseWriter). That could inspect err and if nil, do one thing and if not nil do another. Here’s a playground link with above code:

1 Like

hi @jtuchel

Where do you plan to use this Response struct ? You need to give more details about that, it may be something specific to your app.

The idiomatic way for Golang is to have result, err := methodName(parameters). For an http server you write the status in the header and the result in the response body. Thus, having a struct storing the result and any possible errors, is not something very common.

2 Likes

Sorry @telo_tade , you are right. I want to use this struct for the HTTP response body. This struct won’t contain the HTTP status code. So whenever I want to send back “a value” back to the API consumer I want to use a common response structure. Based on the answer of @Dean_Davidson I tried this one

type OperationResponse[T any] struct {
	IsSuccessful bool   `json:"isSuccessful"`
	Value        T      `json:"value"`
	ErrorCode    string `json:"errorCode,omitempty"`
	Message      string `json:"message,omitempty"`
}

where ErrorCode means something like LicenseExpired. Then I created some util functions

func GetInternalErrorResponse() OperationResponse[*string] {
	return OperationResponse[*string]{
		IsSuccessful: false,
		Value:        nil,
		ErrorCode:    "??? TODO ???",
		Message:      "The server encountered an unexpected condition that prevented it from fulfilling the request.",
	}
}

func GetOperationFailureResponse(errorCode string, message string) OperationResponse[*string] {
	return OperationResponse[*string]{
		IsSuccessful: false,
		Value:        nil,
		ErrorCode:    errorCode,
		Message:      message,
	}
}

func GetOperationSuccessResponse[T any](value T) OperationResponse[T] {
	return OperationResponse[T]{
		IsSuccessful: true,
		Value:        value,
		ErrorCode:    "",
		Message:      "",
	}
}

What do you think?

Sure, that will work.

The alternative approach is this: you always send the response header (OK or a failure status), and for OK headers you send the value/result as JSON in the body. For non OK headers (whatever type of error header) you send in the body the error message (or a specific error message describing the situation - but not giving internal details like the error stack trace or any internal data).

1 Like

No need, JSON serialization has a lot of reflection in it

This is how many public rest APIs work. You only return the “value” as a result on succes with status code 200. And you only return the error with error status code. This creates readable code for the consumer and for the producer.

// Go producer:
result, err := myFunction(...)
if err != nil {
  http.Error(w, toJSON(err), http.InternalServerError)
  return
}

writeJSON(w, result)
return

// helper function:
func toJSON(err error) string {
  cErr := codedError{code: "UnknownError", msg: err.Message()}
  errors.As(err, &cErr) // get details from codedError
  b, jsonErr := json.Marshal(cErr)
  if jsonErr != nil {
    log.Error("this should not happen...")
    return err.Message()
  }
  return string(b)
}
// JavaScript consumer
const resp = await fetch('/api/...')
if (!resp.ok) {
  throw new CodedError(await resp.json())
}

return resp.json()
1 Like

Define a consistent structure for your API responses. This structure should include fields for indicating the status, message, and data (if applicable) of the response.

Include a status field in the response to indicate the outcome of the API request. For example, you can use values like “success” or “error” to represent the overall status.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.