-
Notifications
You must be signed in to change notification settings - Fork 55
Feather architecture
Feather uses the Vapor framework under the hood.
The Feather core library provides all the necessary APIs to build modules.
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.
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.
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(),
])
}
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.
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
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.
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 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.
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.