Loco is a Rust web framework inspired by Ruby on Rails. It provides generators, ORM, background jobs, mailers, and deployment — all the batteries Rails includes, but with Rust's performance and safety.
Why Loco Matters
Rust web development usually means assembling 10+ crates: Axum for routing, SeaORM for database, Tokio for async, custom solutions for jobs, auth, and mailers. Loco bundles everything into one opinionated framework.
What you get for free:
- Rails-like generators (scaffold, model, controller, migration)
- SeaORM integration with automatic migrations
- Background jobs with SidekiqWorker-like API
- Built-in authentication (JWT + session)
- Mailer with templates
- Testing framework
- One-command deployment
- CLI similar to
rails
Quick Start
# Install
cargo install loco-cli
# Create project
loco new my-app
cd my-app
# Generate scaffold (model + controller + migration)
cargo loco generate scaffold post title:string body:text published:bool
# Run migrations
cargo loco db migrate
# Start server
cargo loco start
Controllers (Routes)
use axum::extract::{Path, State};
use loco_rs::prelude::*;
use crate::models::_entities::posts;
async fn list(
State(ctx): State<AppContext>,
) -> Result<Response> {
let posts = posts::Entity::find()
.order_by_desc(posts::Column::CreatedAt)
.all(&ctx.db)
.await?;
format::json(posts)
}
async fn get_one(
Path(id): Path<i32>,
State(ctx): State<AppContext>,
) -> Result<Response> {
let post = posts::Entity::find_by_id(id)
.one(&ctx.db)
.await?
.ok_or_else(|| Error::NotFound)?;
format::json(post)
}
async fn create(
State(ctx): State<AppContext>,
Json(params): Json<CreatePostParams>,
) -> Result<Response> {
let post = posts::ActiveModel {
title: Set(params.title),
body: Set(params.body),
published: Set(params.published),
..Default::default()
}
.insert(&ctx.db)
.await?;
format::json(post)
}
pub fn routes() -> Routes {
Routes::new()
.prefix("posts")
.add("/", get(list))
.add("/:id", get(get_one))
.add("/", post(create))
}
Models (SeaORM)
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, DeriveEntityModel)]
#[sea_orm(table_name = "posts")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub title: String,
pub body: String,
pub published: bool,
pub created_at: DateTimeWithTimeZone,
pub updated_at: DateTimeWithTimeZone,
}
// Migrations are auto-generated
Background Jobs
use loco_rs::prelude::*;
pub struct SendWelcomeEmail;
#[async_trait]
impl Worker<SendWelcomeEmailArgs> for SendWelcomeEmail {
async fn perform(&self, args: SendWelcomeEmailArgs) -> Result<()> {
let user = users::Entity::find_by_id(args.user_id)
.one(&self.ctx.db)
.await?;
mailers::auth::welcome(&user).deliver(&self.ctx).await?;
Ok(())
}
}
// Enqueue a job
SendWelcomeEmail::perform_later(&ctx, SendWelcomeEmailArgs {
user_id: user.id,
}).await?;
Authentication (Built-in)
use loco_rs::prelude::*;
// Protect routes with JWT
async fn protected_route(
auth: auth::JWT,
State(ctx): State<AppContext>,
) -> Result<Response> {
let user = users::Model::find_by_pid(&ctx.db, &auth.claims.pid).await?;
format::json(user)
}
// Login endpoint (auto-generated)
async fn login(
State(ctx): State<AppContext>,
Json(params): Json<LoginParams>,
) -> Result<Response> {
let user = users::Model::find_by_email(&ctx.db, ¶ms.email).await?;
let valid = user.verify_password(¶ms.password);
if !valid {
return unauthorized("Invalid credentials");
}
let token = user.generate_jwt(&ctx.config.auth.jwt.secret, 24)?;
format::json(LoginResponse { token })
}
Testing
#[tokio::test]
async fn test_create_post() {
testing::request::<App, _, _>(|request, ctx| async move {
let response = request
.post("/api/posts")
.json(&serde_json::json!({
"title": "Test Post",
"body": "This is a test",
"published": true
}))
.await;
assert_eq!(response.status_code(), 200);
let body: serde_json::Value = response.json();
assert_eq!(body["title"], "Test Post");
})
.await;
}
Useful Links
Building Rust-powered APIs? Check out my developer tools on Apify for ready-made web scrapers, or email spinov001@gmail.com for custom solutions.
Top comments (0)