Skip to main content

Backend

Familiarizing yourself with concepts in the Architecture Reference is recommended prior to starting development on the backend.

Structure

backend
├─ api # generated code from proto
├─ gateway # JSON/gRPC server for components
├─ middleware # request/response interceptors
├─ mock # mock gateway and components
├─ module # implementations of proto stubs for API endpoints
├─ resolver # resolver implementations
└─ service # interfaces to external services or resources

Components

Components consist primarily of a Name string for registration purposes and a New factory method, which returns an implementation of the interface required for that component.

New

All New methods for components have the following signature:

New(cfg *any.Any, logger *zap.Logger, scope tally.Scope) (<component type>, error)

Configuration must be unmarshaled from the Any type into the native protobuf type for further consumption using anypb.

References to the logger and scope can be saved in the component implementation's struct for later use. Note that the gateway adds a component namespace for the logger and stats scope so it does not have to be added by the developer.

Gateway and Middleware

The gateway is extensible via middleware, which operates on the incoming request and the outgoing response in a single context.

Currently the gateway only supports unary requests (as opposed to streaming). Therefore middleware returns a server unary interceptor.

Modules

Modules are implementations of API stubs generated from proto. The Register method is a callback used by the gateway to add the gRPC and JSON servers to the main gateway server.

Resolvers

Resolvers abstract resource location into an extensible framework. Finding resources is different at almost every organization. Changing the first step of locating a resource should not require wholesale replacement of a feature. That's where resolvers come in.

Resolvers start in protobuf with an object definition representing the user input needed to locate a resource. The fields are additionally annotated with schema information (such as placeholder, required, display_name) at the object and field level so that the frontend knows how to render the appropriate form.

The resolver has two ways of locating resources, Search takes a free-form text query and locates resources by interpreting the query and making the appropriate call. Resolve takes the filled-out schemas and makes a query based on that structured information.

One additional nicety in the resolver is the FanoutHandler which makes managing concurrent requests easier. It has limit handling to return early once the limit is satisfied, cancelling any remaining outstanding requests.

In the future, the resolver will handle autocomplete and asynchronous validation of form input for the frontend.

More docs are coming on developing resolvers. For now look at other resolvers as an example.

Services

Interactions with third-party services or networked dependencies such as a database should always hide behind a service. Services have no other restrictions and the underlying type is simply an empty interface.

All services are placed in a registry for use by other components. Other components that depend on services fetch them in their factory method. For example,

// Create a new instance of my module that uses the foo service.
func New(*any.Any, *zap.Logger, tally.Scope) (module.Module, error) {
si, ok := service.Registry["clutch.service.foo"]
if !ok {
return nil, errors.New("could not find service")
}

c, ok := si.(foo.Client)
if !ok {
return nil, errors.New("service was not the correct type")
}

return &mod{client: c}, nil
}

Custom Code

Because services return interfaces, it is easy to substitute in a custom implementation by creating a new custom component that implements the interface type.

It is also possible to override specific methods of a service.