Controllers
Loco
is a framework that wraps around axum, offering a straightforward approach to manage routes, middlewares, authentication, and more right out of the box. At any point, you can leverage the powerful axum Router and extend it with your custom middlewares and routes.
Controllers and Routing
Adding a controller
Provides a convenient code generator to simplify the creation of a starter controller connected to your project. Additionally, a test file is generated, enabling easy testing of your controller.
Generate a controller:
After generating the controller, navigate to the created file in src/controllers
to view the controller endpoints. You can also check the testing (in folder tests/requests) documentation for testing this controller.
Displaying active routes
To view a list of all your registered controllers, execute the following command:
This command will provide you with a comprehensive overview of the controllers currently registered in your system.
Adding state
Your app context and state is held in AppContext
and is what Loco provides and sets up for you. There are cases where you'd want to load custom data,
logic, or entities when the app starts and be available to use in all controllers.
You could do that by using Axum's Extension
. Here's an example for loading an LLM model, which is a time consuming task, and then providing it to a controller endpoint, where its already loaded, and fresh for use.
First, add a lifecycle hook in src/app.rs
:
// in src/app.rs, in your Hooks trait impl override the `after_routes` hook:
async
Next, consume this state extension anywhere you like. Here's an example controller endpoint:
async
Routes in Controllers
Controllers define Loco routes capabilities. In the example below, a controller creates one GET endpoint and one POST endpoint:
use ;
new
.add
.add
You can also define a prefix
for all routes in a controller using the prefix
function.
Sending Responses
Response senders are in the format
module. Here are a few ways to send responses from your routes:
// keep a best practice of returning a `Result<impl IntoResponse>` to be able to swap return types transparently
pub async json
// use `render` for a builder interface for more involved responses. you can still terminate with
// `json`, `html`, or `text`
render
.etag?
.json
Content type aware responses
You can opt-in into the responders mechanism, where a format type is detected and handed to you.
Use the Format
extractor for this:
pub async
Custom errors
Here is a case where you might want to both render differently based on different formats AND ALSO, render differently based on kinds of errors you got.
pub async
Here, we also "centralize" our error handling by first wrapping the workflow in a function, and grabbing the result type.
Next we create a 2 level match to:
- Match the result type
- Match the format type
Where we lack the knowledge for handling, we just return the error as-is and let the framework render out default errors.
Creating a Controller Manually
1. Create a Controller File
Start by creating a new file under the path src/controllers
. For example, let's create a file named example.rs
.
2. Load the File in mod.rs
Ensure that you load the newly created controller file in the mod.rs
file within the src/controllers
folder.
3. Register the Controller in App Hooks
In your App hook implementation (e.g., App struct), add your controller's Routes
to AppRoutes
:
// src/app.rs
;
Middleware
Authentication
In the Loco
framework, middleware plays a crucial role in authentication. Loco
supports various authentication methods, including JSON Web Token (JWT) and API Key authentication. This section outlines how to configure and use authentication middleware in your application.
JSON Web Token (JWT)
Configuration
By default, Loco uses Bearer authentication for JWT. However, you can customize this behavior in the configuration file under the auth.jwt section.
- Bearer Authentication: Keep the configuration blank or explicitly set it as follows:
# Authentication Configuration auth: # JWT authentication jwt: location: Bearer ...
- Cookie Authentication: Configure the location from which to extract the token and specify the cookie name:
# Authentication Configuration auth: # JWT authentication jwt: location: from: Cookie name: token ...
- Query Parameter Authentication: Specify the location and name of the query parameter:
# Authentication Configuration auth: # JWT authentication jwt: location: from: Query name: token ...
Usage
In your controller parameters, use auth::JWT
for authentication. This triggers authentication validation based on the configured settings.
use *;
async
Additionally, you can fetch the current user by replacing auth::JWT with auth::ApiToken<users::Model>
.
API Key
For API Key authentication, use auth::ApiToken. This middleware validates the API key against the user database record and loads the corresponding user into the authentication parameter.
use *;
async
Remote IP
When your app is under a proxy or a load balancer (e.g. Nginx, ELB, etc.), it does not face the internet directly, which is why if you want to find out the connecting client IP, you'll get a socket which indicates an IP that is actually your load balancer instead.
The load balancer or proxy is responsible for doing the socket work against the real client IP, and then giving your app the load via the proxy back connection to your app.
This is why when your app has a concrete business need for getting the real client IP you need to use the de-facto standard proxies and load balancers use for handing you this information: the X-Forwarded-For
header.
Loco provides the remote_ip
section for configuring the RemoteIP
middleware:
server:
middleware:
# calculate remote IP based on `X-Forwarded-For` when behind a proxy or load balancer
# use RemoteIP(..) extractor to get the remote IP.
# without this middleware, you'll get the proxy IP instead.
# For more: https://github.com/rails/rails/blob/main/actionpack/lib/action_dispatch/middleware/remote_ip.rb
#
# NOTE! only enable when under a proxy, otherwise this can lead to IP spoofing vulnerabilities
# trust me, you'll know if you need this middleware.
remote_ip:
enable: true
# # replace the default trusted proxies:
# trusted_proxies:
# - ip range 1
# - ip range 2 ..
# Generating a unique request ID and enhancing logging with additional information such as the start and completion of request processing, latency, status code, and other request details.
Then, use the RemoteIP
extractor to get the IP:
pub async
When using the RemoteIP
middleware, take note of the security implications vs. your current architecture (as noted in the documentation and in the configuration section): if your app is NOT under a proxy, you can be prone to IP spoofing vulnerability because anyone can set headers to arbitrary values, and specifically, anyone can set the X-Forwarded-For
header.
This middleware is not enabled by default. Usually, you will know if you need this middleware and you will be aware of the security aspects of using it in the correct architecture. If you're not sure -- don't use it (keep enable
to false
).
Secure Headers
Loco comes with default secure headers applied by the secure_headers
middleware. This is similar to what is done in the Rails ecosystem with secure_headers.
In your server.middleware
YAML section you will find the github
preset by default (which is what Github and Twitter recommend for secure headers).
server:
middleware:
# set secure headers
secure_headers:
preset: github
You can also override select headers:
server:
middleware:
# set secure headers
secure_headers:
preset: github
overrides:
foo: bar
Or start from scratch:
server:
middleware:
# set secure headers
secure_headers:
preset: empty
overrides:
foo: bar
Compression
Loco
leverages CompressionLayer to enable a one click
solution.
To enable response compression, based on accept-encoding
request header, simply edit the configuration as follows:
#...
middlewares:
compression:
enable: true
Doing so will compress each response and set content-encoding
response header accordingly.
Precompressed assets
Loco
leverages ServeDir::precompressed_gzip to enable a one click
solution of serving pre compressed assets.
If a static assets exists on the disk as a .gz
file, Loco
will serve it instead of compressing it on the fly.
#...
middlewares:
...
static_assets:
...
precompressed: true
Handler and Route based middleware
Loco
also allow us to apply layers to specific handlers or
routes.
For more information on handler and route based middleware, refer to the middleware
documentation.
Handler based middleware:
Apply a layer to a specific handler using layer
method.
// src/controllers/auth.rs
Route based middleware:
Apply a layer to a specific route using layer
method.
// src/main.rs
;
Pagination
In many scenarios, when querying data and returning responses to users, pagination is crucial. In Loco
, we provide a straightforward method to paginate your data and maintain a consistent pagination response schema for your API responses.
Using pagination
use *;
let res = fetch_page.await;
Using pagination With Filter
use *;
let pagination_query = PaginationQuery ;
let condition = condition.contains;
let paginated_notes = paginate
.await?;
- Start by defining the entity you want to retrieve.
- Create your query condition (in this case, filtering rows that contain "loco" in the title column).
- Define the pagination parameters.
- Call the paginate function.
Pagination view
After creating getting the paginated_notes
in the previous example, you can choose which fields from the model you want to return and keep the same pagination response in all your different data responses.
Define the data you're returning to the user in Loco views. If you're not familiar with views, refer to the documentation for more context.
Create a notes view file in src/view/notes
with the following code:
use ;
use ;
use crate notes;
Testing
When testing controllers, the goal is to call the router's controller endpoint and verify the HTTP response, including the status code, response content, headers, and more.
To initialize a test request, use testing::request
, which prepares your app routers, providing the request instance and the application context.
In the following example, we have a POST endpoint that returns the data sent in the POST request.
async
As you can see initialize the testing request and using request
instance calling /example endpoing.
the request returns a Response
instance with the status code and the response test