Skip to content

[REQ] [go-server] Services knowing about HTTP is questionable #9647

Open
@uykusuz

Description

@uykusuz

Is your feature request related to a problem? Please describe.

Let's look at the service method signatures:

func (s *UserApiService) CreateUser(ctx context.Context, user User) (ImplResponse, error) {
}

The method returns a ImplResponse, carrying a HTTP status code. I see two problems with this.

The first one .... really, really minor: why does a method return an implementation object? It should return an interface, and ImplResponse would be an implementation of that interface.

Now to the other, bigger one.

Imo there's an architectural problem with the idea of controllers and services.

What is the idea of controllers and services?

The idea of the separation between controllers and services is (imo, pls enlighten me otherwise):

  • controllers hold all the connection logic, specific to http in this case.
  • services hold the business logic solely. They don't know anything about HTTP status codes. They return model classes and errors

What's the current state?

Services return response objects, carrying HTTP status codes. Making services HTTP-dependant.

What's wrong with this?

Imagine you have a REST API (openapi) and e.g. a gRPC API. The services do exactly the same, we just have different controllers.

With the current design this forces us to have three layers:

  • the openapi-controller layer
  • the openapi-service layer (HTTP specific)
  • our actual service layer independent of HTTP knowledge.

That middle layer should not be necessary, it bloats up the code.

Describe the solution you'd like

Keeping it implementation-independent, it would be best (also just imo, please enlighten me otherwise) to have service methods like this:

func (s *UserApiService) CreateUser(ctx context.Context, user User) (interface{}, error) {
}

The interface{} is one of the generated model classes, ready for serialization.

Where error can be of a new interface type:

type Error interface {
  Error()

  IsBadUserInput()
  IsInternal()
  IsUpstream()
  // ...
}

Then the controller can check whether the returned error is of that interface and map to corresponding HTTP status codes and if not simply return a 500:

// CreateUser - Create user
func (c *UserApiController) CreateUser(w http.ResponseWriter, r *http.Request) {
	user := &User{}
	if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
		w.WriteHeader(http.StatusBadRequest)
		return
	}
	result, err := c.service.CreateUser(r.Context(), *user)
	// If an error occurred, encode the error with the status code
	if err != nil {
		if knownErr, ok := err.(Error); ok {
			if knownErr.IsBadUserInput() {
				EncodeJSONResponse(err.Error(), &http.StatusBadRequest, w)
			} else if knownErr.IsInternal() {
				// ...
			}
			// aso.
		} else {	
			EncodeJSONResponse(err.Error(), &http.StatusInternalServerError, w)
		}
	} else {
		// If no error, encode the body and the result code
		EncodeJSONResponse(result, &http.StatusOK, w)
	}
}

Describe alternatives you've considered

Not alternatives, but workarounds.

One workaround is the aforementioned, three-layer-architecture, that bloats up the code.

Another one is ignoring the ImplResponse:

  • either redefine ImplResponse to interface{}
  • or ignore the status code in ImplResponse and only use body

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions