REST API Controller
MSX promotes the usage of the common Controller > Service > Repository layered architecture within microservices.
The role of the Controller is to accept REST-based API requests from callers (UI, swagger, other microservices), and route them to the service.
To generate a complete domain, including the controller, use the skel tool.
In the Services section, we described the various components of every service, including REST API Controllers. The following sections assumes your familiarity with those components.
REST Controllers have specific requirements for:
- Interface Definition
- Mock
- Dependencies
- Implementation
- Lifecycle Registration (for root components)
Interface Definition
The REST controller component is never mocked as it is a root component. No interface definition is required.
Mock
The REST controller component is never mocked as it is a root component. No mock generation is required.
Dependencies
REST Controllers typically depend exclusively on the Application Service. For example:
type applicationController struct {
applicationService ApplicationServiceApi
}
The converter dependency has moved to the Application Service, which now receives and returns DTOs.
Implementation
EndpointsProducer
Modern go-msx REST Controllers must implement the restops.EndpointsProducer interface.
The Endpoints function returns a list of endpoints implemented by the controller:
func (c *applicationController) Endpoints() (restops.Endpoints, error) {
builders := restops.EndpointBuilders{
c.getConfigApplicationResultsByConfigApplicationId(),
...
}
return builders.Endpoints()
}
Each endpoint or builder is generated by calling a method on the controller from
the Endpoints function. These are then aggregated and returned as a slice of Endpoints.
EndpointTransformersProducer
A controller may also implement the restops.EndpointTransformersProducer interface
in order to apply transformations to each of the registered endpoints, including tagging
and path manipulation:
func (c *applicationController) EndpointTransformers() restops.EndpointTransformers {
openapi.AddTag("Applications", "Configuration Applications Controller")
return restops.EndpointTransformers{
restops.AddEndpointPathPrefix(pathPrefixConfiguration),
restops.AddEndpointTag("Applications"),
}
}
Current transformers are:
AddEndpointTag: Adds a tag to each endpointAddEndpointPathPrefix: Adds a prefix to the path of each endpointAddEndpointErrorConverter: Sets a custom ErrorConverter for each endpointAddEndpointErrorCoder: Sets a custom ErrorCoder for each endpointAddEndpointContextInjector: Adds a Context injector to each endpointAddEndpointMiddleware: Adds an HTTP Middleware to each endpoint
EndpointBuilder
Each endpoint in the controller can be generated using one of the provided EndpointBuilder
instances. go-msx provides builders for each active API style:
- v2: Uses response envelopes and v2 pagination style
- v8: Uses no response envelopes, v8 error response format, and v8 pagination style
Endpoint Types
Each API style builder provides a number of different endpoint types:
- List: Return a series of entities matching a given criteria
- Retrieve: Returns a single entity matching a primary key
- Create: Instantiates a new entity using the supplied payload
- Update: Replaces an existing entity using the supplied payload
- Delete: Destroys an existing entity matching a primary key
- Command: Executes an operation specific to the entity domain
Example
Here is an example (from lanservice) of a simple "Command" endpoint:
func (c *applicationController) applyConfiguration() restops.EndpointBuilder {
type inputs struct {
Id types.UUID `req:"path"`
IncludeSubTenants bool `req:"query" required:"false" default:"false" description:"Include Sub Tenants"`
}
type outputs struct {
Body api.ConfigurationResponse `resp:"body"`
}
return v2.
NewCommandEndpointBuilder(pathSuffixConfigurationId, "applications").
WithId("applyConfigurationForTenant").
WithInputs(inputs{}).
WithOutputs(outputs{}).
WithPermissions(permissionManageSwitchConfigurations).
WithDoc(new(openapi3.Operation).
WithSummary("Apply configuration to a tenant")).
WithHandler(func (ctx context.Context, inp *inputs) (out outputs, err error) {
out.Body, err = c.applicationService.
ApplyConfiguration(ctx, inp.Id, inp.IncludeSubTenants)
return
})
}
Above, you can see a number of different components:
- input port structure: defines the fields to be retrieved from the request
- output port structure: defines the fields to be applied to the response
- builder: simplifies creating endpoints
- operation name: defines the operation key in OpenApi and Tracing
- permissions: enumerates the possible passing permission(s)
- handler: called when the endpoint is activated
- documentation: populates the OpenApi documentation
Handler
The Endpoint Handler accepts functions with arbitrary arguments, which it will fill out by matching the argument type. These can include:
context.Context: The context of the request*http.Request: The inbound HTTP request being handled, allowing for manual request parsinghttp.ResponseWriter: The outbound HTTP response to return, allowing for manual response handling*inputs: The Input Port structure declared by a call toWithInputs, containing the populated inputs
The Endpoint Handler also accepts functions with arbitrary return values, which it will consume:
outputs: The Output Port structure declared by a call toWithOutputs, which you can populate with response outputs.error: An error to be applied to the defined (or style default) error response body.
If the Output Port structure is excluded from the declaration of
your return values, you are expected to use an http.ResponseWriter to manually
send the success response (or return an error).
If both the Output Port structure and error are excluded, you are expected
to manually send a response (whether error or success).
Standard Practice
The most common format for handler function includes context, inputs, outputs, and error:
.WithHandler(
func (ctx context.Context, inp *inputs) (out outputs, err error) {
...
})
Manual Handling
To manually handle the request/response cycle, use a standard go HTTP handler:
.WithHttpHandler(
func (resp http.ResponseWriter, req *http.Request){
...
})
Custom Validation
Endpoint parameter validation can be performed in two ways:
-
Define struct tags on each field declaring the jsonschema validation that is required. This is used to validate the format of strings, enumerations, etc.
-
Any member of the
inputsstruct passed intoEndpoint.Inputswhich implement thevalidate.Validatableinterface will be validated before being passed into the controller. Create and Update request bodies with complex inter-field interactions will typically use this. Common validators are provided by the github.com/go-ozzo/ozzo-validation package. A few custom validators are available in the validate package.
Any non-nil errors returned by the validation function will cause an instance of ValidationErrors
to be sent back to the client (with a 400 Bad Request header) detailing the errors.
Response Codes
Success Responses
To use the default success status code (determined by which builder you used), no implementation is required.
To override the success code, add a Code int resp:"code" field to your output port struct
and populate it before returning from your handler.
Error Responses
REST operations have built-in default error coder, which you can override using a custom error mapper or error coder.
Default mappings include:
| Error | Code |
|---|---|
js.ErrValidationFailed | 400 |
ops.ErrMissingRequiredValue | 400 |
rbac.ErrTenantDoesNotExist | 401 |
rbac.ErrUserDoesNotHaveTenantAccess | 401 |
repository.ErrAlreadyExists | 409 |
repository.ErrNotFound | 404 |
Lifecycle Registration
In order to instantiate your controller during application startup, you can register a simple
init function:
func init() {
app.OnCommandsEvent(
[]string{
app.CommandRoot,
app.CommandOpenApi,
},
app.EventStart,
app.PhaseBefore,
func (ctx context.Context) error {
controller, err := newApplicationController(ctx)
if err != nil {
return err
}
return restops.
ContextEndpointRegisterer(ctx).
RegisterEndpoints(controller)
})
}
This will register your controller during normal microservice startup, as well as during OpenApi spec generation.
To ensure your module is included in the built microservice, include the module from your main.go:
import _ "cto-github.cisco.com/NFV-BU/lanservice/internal/application"