Add --service flag to cargo loco generate for generating a service layer
Author: newmizanurCreated Oct 23, 2025Updated Jul 29, 2026
Labelsenhancement
Description
Currently, cargo loco generate can create controllers and other resources (e.g. cargo loco generate controller auth --kind api), but there’s no built-in way to generate a service layer.
Adding a --service flag would let devs automatically generate a corresponding service module alongside the controller, promoting cleaner separation of business logic.
Proposed Feature
Add an optional --service flag to generator commands, for example:
cargo loco generate controller auth --kind api --serviceThis would generate:
src/
controllers/
auth.rs
services/
auth.rs- The controller wires HTTP to service calls.
- The service wraps business logic and returns domain results / errors.
Minimal Example (what --service could generate)
controllers/auth.rs (minimal)
use crate::{
error_code::{auth_errors, AppError},
models::users::{LoginParams, RegisterParams},
services::auth::{AuthService, Session},
views::auth::{CurrentResponse, LoginResponse},
};
use axum::{response::IntoResponse, Json};
use loco_rs::prelude::*;
#[derive(serde::Serialize)]
struct ErrorBody {
code: u16,
message: &'static str,
}
fn to_http_error(err: AppError) -> Result<Response> {
let status = err.http_status();
Ok((status, Json(ErrorBody { code: err.code, message: err.message })).into_response())
}
#[debug_handler]
async fn register(State(ctx): State<AppContext>, Json(params): Json<RegisterParams>) -> Result<Response> {
match AuthService::register(&ctx, params).await {
Ok(()) => format::json(()),
Err(e) => to_http_error(e),
}
}
#[debug_handler]
async fn login(State(ctx): State<AppContext>, Json(params): Json<LoginParams>) -> Result<Response> {
match AuthService::login(&ctx, params).await {
Ok(Session { user, token }) => format::json(LoginResponse::new(&user, &token)),
Err(e) => to_http_error(e),
}
}
#[debug_handler]
async fn current(auth: auth::JWT, State(ctx): State<AppContext>) -> Result<Response> {
match AuthService::current(&ctx, &auth.claims.pid).await {
Ok(user) => format::json(CurrentResponse::new(&user)),
Err(e) => to_http_error(e),
}
}
pub fn routes() -> Routes {
Routes::new()
.prefix("/api/auth")
.add("/register", post(register))
.add("/login", post(login))
.add("/current", get(current))
}services/auth.rs (minimal)
use crate::{
error_code::{AppResult, auth_errors},
models::{
_entities::users,
users::{LoginParams, RegisterParams},
},
};
use loco_rs::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize)]
pub struct Session {
pub user: users::Model,
pub token: String,
}
pub struct AuthService;
impl AuthService {
pub async fn register(ctx: &AppContext, params: RegisterParams) -> AppResult<()> {
users::Model::create_with_password(&ctx.db, ¶ms)
.await
.map(|_| ())
.map_err(|_| auth_errors::DB_FAILURE)
}
pub async fn login(ctx: &AppContext, params: LoginParams) -> AppResult<Session> {
let user = users::Model::find_by_email(&ctx.db, ¶ms.email)
.await
.map_err(|_| auth_errors::INVALID_CREDENTIALS)?;
if !user.verify_password(¶ms.password) {
return Err(auth_errors::INVALID_CREDENTIALS);
}
let jwt_cfg = ctx.config.get_jwt_config().map_err(|_| auth_errors::DB_FAILURE)?;
let token = user.generate_jwt(&jwt_cfg.secret, jwt_cfg.expiration)
.map_err(|_| auth_errors::INVALID_CREDENTIALS)?;
Ok(Session { user, token })
}
pub async fn current(ctx: &AppContext, pid: &str) -> AppResult<users::Model> {
users::Model::find_by_pid(&ctx.db, pid)
.await
.map_err(|_| auth_errors::DB_FAILURE)
}
}Benefits
- Separates controllers (transport) from services (business logic).
- Reduces repetitive boilerplate for common service patterns.
- Aligns with common backend architecture (controller → service → model/repo).
Additional Notes
The controller template can auto-import and call the generated service.
Future follow-ups could add:
cargo loco generate service <name>for standalone service generation.
Source: loco-rs/loco