Help language development. Donate to The Perl Foundation

cro zef:cro last updated on 2022-11-30

docs/structuring-services.md
# Structuring services built with Cro

Setting up a simple Cro service serving a handful of routes is pretty easy:
stick them into a `route` block, pass it to the HTTP server, and that's it.
The `cro stub` HTTP templates place that `route` block in a module, leaving
the server setup to be done in a `service.raku` script. This means that unit
tests can `use` the module with the `route` block, and tests can be written
using `Cro::HTTP::Test`.

This will suffice for small, simple services. However, it won't make for a
maintainable system once one reaches tens or hundreds of routes. This
document explores some ways to structure larger applications and services,
and keep them testable.

## Cro is not a framework

Cro is a set of tools and libraries, rather than a framework. This may sound
like an academic point, and Cro's designers fully expect it will be variously
called a "web framework", "service framework", and so forth. However, the
distinction does come with some consequences.

The line between library and framework is blurry, but in general, frameworks
own the "main loop" of a program and call your code, while libraries are code
that you call. Further, frameworks tend to assume that applications revolve
around them, and so have no qualms about providing an overall structure. By
contrast, libraries expect to be just one ingredient in the program pie. Thus,
Cro does not try to impose an overall architecture on programs using it.

Frameworks do, however, provide some comfort that one is heading in a
reasonable direction. This document provides design/architecture suggestions
for those building Cro HTTP services and applications, and wondering how one
might structure them as they become larger.

## Keep route handlers focused

Much of software design is about working out what belongs where. Cro route
handlers are the place to implement the mapping between HTTP and your model
(where the model may be a domain model, a data model, etc.) Thus, it's quite
reasonable for `route` handlers to:

* Enforce authentication/authorization (typically through a `subset` type on
  the request auth object)
* Map the request to an appropriate bit of model logic
* Transform the data in the request into a command object or model object
* Map the results of an operation into a HTTP response
* Map any model errors into the appropriate HTTP error

However, avoid:

* Doing database queries directly from the `route` handler
* Putting business logic in the route handler

Instead, factor these out into separate functions or objects, as appropriate.
This means it is possible to test the route handlers and, potentially, the
business logic in isolation. It also will make it easier to refactor, and to
re-use the same logic in a non-Cro context in the future if required.

## Don't have one giant `route` block

The `Cro::HTTP::Router` provides both `include` and `delegate` for composing
applications out of many `route` blocks. So, instead of:

```
sub routes() is export {
    route {
        get -> 'images', *@path {
            static 'resources/images', @path;
        }

        get -> 'css', $file {
            static 'frontend-build/css', $file;
        }

        get -> 'product', $id {
            ... "route handler goes here"
        }
    }
}
```

One can pull the route handlers for serving assets out:

```
sub routes() is export {
    route {
        include assets();

        get -> 'product', $id {
            ... "route handler goes here"
        }
    }
}

sub assets() {
    route {
        get -> 'images', *@path {
            static 'resources/images', @path;
        }

        get -> 'css', $file {
            static 'frontend-build/css', $file;
        }
    }
}
```

These can be spread over multiple modules. Both `include` and `delegate`
support adding a prefix of one or more route segments to the URI also,
meaning there's no need to repeat it in every single route.

```
use Routes::Assets;
use Routes::Catalogue;
use Routes::Checkout;

sub routes() is export {
    route {
        # No prefix
        include asset-routes();
        # /catalogue prefix
        include 'catalogue' => catalogue-routes();
        # /shop/checkout prefix
        include <shop checkout> => checkout-routes();
    }
}
```

Nested uses of `include` and `delegate` are permitted, to allow recursive
application of this approach.

With `include`, the routes from the nested router are included into the same
URI matcher, so in that sense it's much like they are textually included in
the `route` block that performs the `include`. However, any `before-matched`
and `after-matched` middleware inside of an included `route` block will only
apply to the routes inside of that block. For more details on middleware
handling, and on when to use `delegate` instead of `include`, see the section
on composing routes in the `Cro::HTTP::Router` documentation.

## A functional structure for dependencies

An application will not just have route handlers. Instead, the route handlers
will dispatch operations to, or request results from, other components (for
example, data access layers, data models, or domain objects). To enable
testing of `route` blocks in isolation from these, it is advisable to take
them as arguments to the `sub` enclosing the `route` block.

```
sub user-routes(MyApp::UserDB $db, MyApp::EmailSender $email) {
    route {
        get -> LoggedInUser $user, 'profile' {
            content 'application/json', $db.get-user-profile($user.id);
            CATCH {
                when X::MyApp::NoSuchUser {
                    not-found;
                }
            }
        }

        ...
    }
}
```

This facilitates testing of the logic in the route handlers in isolation from
these dependencies, for example by stubbing them using a module such as
`Test::Mock`.

These dependencies can in turn be passed along as needed to other `sub`s that
contain `route` blocks to be included.

Finally, the top-level entry point of the application initializes the real
versions of the various dependencies, and passes them in.

## An object-oriented structure

One might prefer a more object-oriented approach. This may come in useful if
wishing to use an existing dependency injection container, instead of passing
the dependencies down explicitly from the top level (which is fine up to a
point, but risks getting out of hand in a large application).

Imagine we have a DI container where an `is injected` trait specifies a
dependency provided by the container. We could instead place our `route`
block into a method inside of this, and use the dependencies.

```
unit class Routes::User;

has MyApp::UserDB $.db is injected;
has MyApp::EmailSender $.email is injected;

method routes() {
    route {
        get -> LoggedInUser $user, 'profile' {
            content 'application/json', $!db.get-user-profile($user.id);
            CATCH {
                when X::MyApp::NoSuchUser {
                    not-found;
                }
            }
        }
        ...
    }
}
```

A top-level `Routes` class might then depend on the various other route handler
classes.

```
unit class Routes;

has Routes::User $.user-routes is injected;

method routes() {
    route {
        include 'user' => $!user-routes.routes();
    }
}
```

One then writes tests by constructing the objects with test doubles (mocks,
stubs, etc.) instead of the real thing that the container would use. The
top-level `route` block is obtained by having the container construct the
dependency tree, and calling the `routes` method on the resulting object.

## A note on concurrency

Route handlers may run in parallel, on multiple threads. This means that any
state held inside of the `sub` or `class` holding the route block must be
OK with that.

If the model logic consists of making calls to a database, then there's not
much to worry about so long as the underlying database drivers are good with
that (for example, `DB::Pg` will handle this situation well). However, if the
application has in-memory state, it will need to be protected.