Route Handlers

Route Handlers are a collection of ExpressJS middleware functions whose job it is to process an incoming request for a given RESTful endpoint. Collections of handlers are grouped together by a common name or schema, depending on the OpenAPI specification defined.

Composer makes heavy use of both functional and aspect oriented programming techniques. Each Operation defined in the OpenAPI specification is generated to a single function and is decorated with the necessary TypeScript decorators to indicate their function.

Anatomy of a Route

For example, given the following Operation object defined in an OpenAPI specification file is for a GET request to retrieve a single object resource of the Pet schema given a specified unique identifier.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
 /pet/{id}:
     x-schema: Pet
     parameters:
         - $ref: "#/components/parameters/id"
     get:
         description: Returns a single Pet from the system that the user has access to
         x-name: findById
         responses:
             "200":
                 description: A Pet object.
                 content:
                     application/json:
                         schema:
                             $ref: "#/components/schemas/Pet"

The resulting route handler class and function as generated by Composer will look like the following.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
 /**
  * Handles all REST API requests for the endpoint `/pet`.
  *
  * @author AcceleratXR, Inc.
  */
 @Model(Pet)
 @Route("/pet")
 class PetRoute extends ModelRoute<Pet> {
         ...

         /**
          * Returns a single Pet from the system that the user has access to
          */
         @Get("/:id")
         private async findById(@Param("id") id: string, @AuthUser user?: JWTUser): Promise<Pet | undefined> {
             return super.doFindById(id, user);
         }

         ...
 }

First notice how the class itself is constructed. The Path Item in the OpenAPI specification specifies an x-schema field with value Pet. This means that all Path Items that also specify Pet will be grouped into the same PetRoute.ts file.

The @Model(Pet) decorator is used to identify that this Route handler class is associated with the Pet data model. When the Composer server starts up and scans this class it therefore uses this information to bind the correct datastore connection.

The class also uses the @Route decorator. This indicates to the Server that the class is responsible for processing RESTful endpoints. The decorator can take either a single string value or an array as its sole parameter corresponding to the root path patterns that the handler will respond to. In this example, this route handler is responsible for processing all incoming requests to the /pet path.

Finally we get to the endpoint handler itself, findById. The name findById directly corresponds to the x-name value specified in the Operation object’s definition. Further, the HTTP method is denoted by the @Get decorator and take an optional argument /:id. This path gets appended to the root defined at the top of the class /pet/:id when the class is processed by the Server and registered with ExpressJS. Each function defined with a corresponding HTTP method decorator is registered to ExpressJS accordingly as a middleware function. Should the handler also make use of the @Before and @After decorators, these additional functions will be registered respectively with the route handler function as additional middleware for the given endpoint path.

The function itself takes multiple arguments, namely @Param("id") id: string, @AuthUser user?: JWTUser. The First argument with the @Param decorator indicates that the path contains one or more parameters, one of which is named id and that the server should pull that value out of the request path and pass its value in directly to this argument. Therefore, if the HTTP request that arrives is for the path /pet/scotty then the value of the id argument will be scotty.

The second argument is for the authorized user that performed the request as denoted by the @AuthUser decorator. As this is an optional argument, a value is only passed in when a user has actually authenticated with the service and is valid. In all other cases this value will be undefined to indicate that no valid user is attempting to perform this action.

The final thing to notice about this function is the body and return type. The return type is of type Promise<Pet | undefined>. This indicates to the server that the handler is async and will return a Pet object if the object was found with the given id or undefined object if it could not be at some point in the future. The return of the function does not have to be a Promise and in fact can be a direct value. The server will automatically adjust its behavior accordingly. When one of these values is returned, the server will automatically encode the object as JSON and return it to the client.

The body of the function makes a single call to super.doFindById(id, user). Notice that the class’s definition inherits from ModelRoute. This is a base class to which all Model route handlers can extend to provide common built-in behavior such as this. It’s purpose is to reduce the amount of code needed for common data oriented REST APIs. In this particular case we are leveraging the built-in function doFindById. This function performs generic logic for retrieving a single record of the desired type from the datastore. The built-in will also handle validating permissions for accessing the object for the authorized user when Access Control Lists are enabled.

Route Decorators

The following is a list of decorators that can used within a Route handler class to perform various HTTP processing behavior.

@Route

The @Route decorator is used to indicate that a given class contains one or more endpoint handlers for a given set of paths.

1
2
3
4
5
6
7
8
9
/**
 * Handles all REST API requests for the endpoint `/pet`.
 *
 * @author <AUTHOR>
 */
@Model(Pet)
@Route("/pet")
class PetRoute extends ModelRoute<Pet> {
}

@Init

The @Init decorator indicates a function within a Route handler class that should be called at service startup in order to initialize some state.

1
2
3
4
5
6
7
/**
  * Called on server startup to initialize the route with any defaults.
  */
 @Init
 private async initialize() {
     // TODO Add business logic here
 }

@Auth

The @Auth decorator is used to indicate that the decorated endpoint handle requires authentication by one of the specified methods.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
 /**
  * Add a new pet to the store
  */
 @Auth(["jwt"])
 @Post()
 private async add(obj: Pet, @AuthUser user?: JWTUser): Promise<Pet> {
     const newObj: Pet = new Pet(obj);

     throw new Error("This route is not implemented.");
 }

@Before

This decorator is used to indicate additional middleware functions that should be executed before the main endpoint handler is executed. This is typically used for input pre-processing and validation.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
 /**
  * Add a new pet to the store
  */
 @Auth(["jwt"])
 @Before(["validate"])
 @After(["prepareOutput"])
 @Post()
 private async add(obj: Pet, @AuthUser user?: JWTUser): Promise<Pet> {
     const newObj: Pet = new Pet(obj);

     throw new Error("This route is not implemented.");
 }

@After

The @After decorator is used to indicate additional middleware functions that should be expected after the main endpoint handler is executed. This is typically used for post-processing and data preparation before returning to the client.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
 /**
  * Add a new pet to the store
  */
 @Auth(["jwt"])
 @Before(["validate"])
 @After(["prepareOutput"])
 @Post()
 private async add(obj: Pet, @AuthUser user?: JWTUser): Promise<Pet> {
     const newObj: Pet = new Pet(obj);

     throw new Error("This route is not implemented.");
 }

@Delete

The @Delete decorator is used to indicate that the handler function will process HTTP requests with method DELETE. It takes an optional parameter to provide a sub-path.

1
2
3
4
5
6
7
8
 /**
  * Deletes the Pet
  */
 @Auth(["jwt"])
 @Delete("/:id")
 private async delete(@Param("id") id: string, @AuthUser user?: JWTUser): Promise<void> {
     return super.doDelete(id, user);
 }

@Get

The @Get decorator is used to indicate that the handler function will process HTTP requests with method GET. It takes an optional parameter to provide a sub-path.

1
2
3
4
5
6
7
 /**
  * Multiple Pet objects
  */
 @Get()
 private async find(@AuthUser user?: JWTUser): Promise<Array<Pet>> {
     throw new Error("This route is not implemented.");
 }

@Post

The @Post decorator is used to indicate that the handler function will process HTTP requests with method POST. It takes an optional parameter to provide a sub-path.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
 /**
  * Add a new pet to the store
  */
 @Auth(["jwt"])
 @Post()
 private async add(obj: Pet, @AuthUser user?: JWTUser): Promise<Pet> {
     const newObj: Pet = new Pet(obj);

     throw new Error("This route is not implemented.");
 }

@Put

The @Put decorator is used to indicate that the handler function will process HTTP requests with method PUT. It takes an optional parameter to provide a sub-path.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
 /**
  * Updates a single Pet
  */
 @Auth(["jwt"])
 @Put("/:id")
 @Validate("validate")
 private async update(@Param("id") id: string, obj: Pet, @AuthUser user?: JWTUser): Promise<Pet> {
     const newObj: Pet = new Pet(obj);

     return super.doUpdate(id, newObj, user);
 }

@Validate

This decorator is used to indicate a middleware function that will execute before the main endpoint handler in order to validate the incoming data. It must be the name of a function within the class itself.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
/**
  * Determines if the specified request payload is valid and can be accepted.
  *
  * @throws When the request payload contains invalid input or data.
  */
 private validate(data: Pet): void {
     // TODO Validate input data
 }

 /**
  * Updates a single Pet
  */
 @Auth(["jwt"])
 @Put("/:id")
 @Validate("validate")
 private async update(@Param("id") id: string, obj: Pet, @AuthUser user?: JWTUser): Promise<Pet> {
     const newObj: Pet = new Pet(obj);

     return super.doUpdate(id, newObj, user);
 }