riverqueue / riverui

A web interface for River, fast and reliable background jobs in Go.
https://ui.riverqueue.com/
Mozilla Public License 2.0
43 stars 3 forks source link

Introduce lightweight API framework for Go code + test suite #63

Closed brandur closed 4 days ago

brandur commented 1 week ago

Put in a lightweight API framework for River UI's Go code to make writing endpoints more succinct and better enable testing. Endpoints are defined as a type that embeds a struct declaring their request and response types along with metadata that declares their path and success status code:

type jobCancelEndpoint struct {
        apiBundle
        apiendpoint.Endpoint[jobCancelRequest, jobCancelResponse]
}

func (*jobCancelEndpoint) Meta() *apiendpoint.EndpointMeta {
        return &apiendpoint.EndpointMeta{
                Path:       "POST /api/jobs/cancel",
                StatusCode: http.StatusOK,
        }
}

The request/response types know to unmarshal and marshal themselves from to JSON, or encapsulate any path/query parameters they need to capture:

type jobCancelRequest struct {
        JobIDs []int64String `json:"ids"`
}

type jobCancelResponse struct {
        Status string `json:"status"`
}

Endpoints define an Execute function that takes a request struct and returns a response struct along with a possible error:

func (a *jobCancelEndpoint) Execute(ctx context.Context, req *jobCancelRequest) (*jobCancelResponse, error) {
    ...

    return &jobCancelResponse{Status: "ok"}, nil
}

This makes the endpoints a lot easier to write because serialization code gets removed, and errors can be returned succinctly according to normal Go practices instead of each one having to be handled in a custom way and be a liability in case of a forgotten return after writing it back in the response. The underlying API framework takes care of writing back errors that should be user-facing (anything in the newly added apierror package) or logging an internal error and return a generic message. Context deadline code also gets pushed down.

The newly added test suite shows that the Execute shape also makes tests easier and more succinct to write because structs can be sent and read directly without having to go through a JSON/HTTP layer, and errors can be handled directly without having to worry about them being converted to a server error, which makes debugging broken tests a lot easier.

resp, err := endpoint.Execute(ctx, &jobCancelRequest{JobIDs: []int64String{int64String(insertRes1.Job.ID), int64String(insertRes2.Job.ID)}})
require.NoError(t, err)
require.Equal(t, &jobCancelResponse{Status: "ok"}, resp)

We also add a suite of integration-level tests that test each endpoint through the entire HTTP/JSON stack to make sure that everything works at that level. This suite is written much more sparingly -- one test fer endpoint -- because the vast majority of endpoint tests should be written in the handler-level suite for the reasons mentioned above.

brandur commented 1 week ago

@bgentry This isn't done, but I wanted to make sure you're okay with the broad design before going any further.

This is basically how I've set up other API projects in Go. There's definitely a little more abstraction compared to the raw handlers, but IMO has huge benefits for writing much more succinct implementations and testability. It's also potentially introspectable in case we ever want to build an OpenAPI spec for anything like that (in that it's possible to use reflect to iterate over the endpoint/request/response structs and extract information about them).

brandur commented 1 week ago

@bgentry Okay, great! Let me add a little more testing and docs for the core APU infrastructure and then I'll kick it back to you.

I might do the rest of the endpoint conversion in a different PR since with the tests especially, it'll be a big diff unto itself.

brandur commented 6 days ago

@bgentry Okay, added docs and tests for all the core infrastructure. I tried to write a few more API endpoint tests too, but found I needed the changes in https://github.com/riverqueue/river/pull/402 for anything queue-related, so going to push those for now. Mind taking another look?

brandur commented 4 days ago

Thanks for the review!