Skip to content

Feather architecture

Tibor Bödecs edited this page Mar 30, 2022 · 1 revision

Feather uses the Vapor framework under the hood.

The Feather core library provides all the necessary APIs to build modules.

Boot & start

During the boot Feather will gather all the required environmental variables and configure everything needed to properly run the system, including hostname, port, paths and other settings.

# .env example
FEATHER_WORK_DIR=/Users/me/feather
FEATHER_HTTPS=false
FEATHER_HOSTNAME="0.0.0.0"
FEATHER_PORT=8080
FEATHER_PUBLIC_URL="https://feathercms.com"
FEATHER_MAX_BODY_SIZE=10mb
FEATHER_DISABLE_FILE_MIDDLEWARE=true
FEATHER_DISABLE_API_SESSION_MIDDLEWARE=true

Feather requires a proper database & file storage driver configuration.

The system will also setup the public file middleware if needed, this way the server can use the Public directory to host files without the need of a proxy such as nginx.

Feather also configures the session related middlewares, using the fluent database driver.

Then the module manager will load all the enabled modules and copy the required public files and resources to the proper place using the module bundles.

After the core system is ready the module manager will call the boot function of each module.

The next step is the module configuration phase, this is where you can config your route handlers.

Finally, Feather will try to migrate the database and wait until the migration is completed.

From this point modules can take over and the server is ready to handle incoming requests.

System routes

The underlying system module provides a dynamic routing system. You can use hook functions to register new route handlers. More about hooks later on.

  • admin
    • public
    • protected
  • api
    • public
    • protected
  • web (no path component)

The public route components are available to everyone without further restrictions. Protected routes are protected by guard middlewares and only available to users with a given permission.

You can also register custom middlewares for each route, this is done via hooks.

New module

You can create a new module and verify if it's working by printing something in the boot method.

import Feather
import Vapor

struct TestModule: FeatherModule {
    
    func boot(_ app: Application) throws {
        print("test module is working")
    }
}

You can enable this newly created module by adding it to the start method in the main.swift file.

public func configure(_ app: Application) throws {
    // ...

    try app.feather.start([
        UserModuleFactory.build(),
        WebBuilder().build(),
        RedirectBuilder().build(),
        TestModule(),
    ])
}

Custom route handlers

If you want to add a custom route handler, you should register a hook function.

import Feather
import Vapor

struct TestModule: FeatherModule {
    
    func boot(_ app: Application) throws {
        app.hooks.register(.webRoutes, use: webRoutesHook)
    }

    func webRoutesHook(args: HookArguments) {
        args.routes.get("foo", use: fooHandler)
    }
    
    func fooHandler(_ req: Request) async throws -> String {
        "foo"
    }
}

This will display a simple "foo" text when visiting the http://localhost:8080/foo address.

Simple JSON response

You can return with any Codable object if you also add the Content conformance.

import Feather
import Vapor
import SwiftHtml

struct Foo: Codable, Content {
    var bar: String
    var baz: String
}

struct TestModule: FeatherModule {
    
    func boot(_ app: Application) throws {
        app.hooks.register(.publicApiRoutes, use: publicApiRoutesHook)
    }

    func publicApiRoutesHook(args: HookArguments) {
        args.routes.get("foo", use: fooHandler)
    }
    
    func fooHandler(_ req: Request) async throws -> Foo {
        .init(bar: "bar", baz: "baz")
    }
}

This will result in a JSON response, it's attached to the public api endpoint, so the response is going to be available on the following address: http://localhost:8080/api/foo

Simple HTML response

We can use the built-in template engine and the system index template to display HTML content.

import Feather
import Vapor
import SwiftHtml

struct TestModule: FeatherModule {
    
    func boot(_ app: Application) throws {
        app.hooks.register(.webRoutes, use: webRoutesHook)
    }

    func webRoutesHook(args: HookArguments) {
        args.routes.get("foo", use: fooHandler)
    }
    
    func fooHandler(_ req: Request) async throws -> Response {
        let template = req.templateEngine.system.index(.init(title: "Foo")) {
            Wrapper {
                Container {
                    H1("Foo")
                    P("bar, baz, etc.")
                }
            }
        }
        return req.templates.renderHtml(template)
    }
}

You can use the templateEngine to get module provided templates and the req.templates.renderHtml method can render these template files into HTML.

Admin routes

If we use the adminRoutes hook we can add new admin pages to the system.

import Feather
import Vapor
import SwiftHtml

struct TestModule: FeatherModule {
    
    func boot(_ app: Application) throws {
        app.hooks.register(.adminRoutes, use: adminRoutesHook)
    }

    func adminRoutesHook(args: HookArguments) {
        args.routes.get("foo", use: fooHandler)
    }
    
    func fooHandler(_ req: Request) async throws -> Response {
        let template = req.templateEngine.system.index(.init(title: "Foo")) {
            Wrapper {
                Container {
                    H1("Foo")
                    P("bar, baz, etc.")
                }
            }
        }
        return req.templates.renderHtml(template)
    }
}

Admin URLs are protected by the user module, if the module is enabled you have to log in in order to visit the admin interface, otherwise if you disable the user module admin URLs will be publicly available and you have to provide your own guard logic by registering a custom admin middleware.

For now sign in with your account and visit the http://localhost:8080/admin/foo page.

If you don't have the right permission you'll be redirected to the home screen.

Hook functions

Hook functions are based on the event-driven-architecture, you can read more about the architecture and the relation with modules here.

In a nutshell you can register for an event using the register or registerAsync function and you pass a handler function. If the handler's function signature matches the invoked event signature then your function will be called and the return type can be used for further actions.

Custom async response handler hook

It is possible to provide a custom response handler hook for the web routes, this way you can handle dynamic URL paths if needed.

import Feather
import Vapor
import SwiftHtml

struct TestModule: FeatherModule {
    
    func boot(_ app: Application) throws {
        app.hooks.registerAsync(.response, use: fooHandler)
    }

    func fooHandler(_ args: HookArguments) async throws -> Response? {
        let template = args.req.templateEngine.system.index(.init(title: "404")) {
            Wrapper {
                Container {
                    H1("404")
                    P(args.req.url.path)
                }
            }
        }
        return args.req.templates.renderHtml(template)
    }
}

The snippet above will return with a custom page for every URL so it acts like a custom 404 page.