#1673·loco

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:

bash
cargo loco generate controller auth --kind api --service

This 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)

rust
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)

rust
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, &params)
            .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, &params.email)
            .await
            .map_err(|_| auth_errors::INVALID_CREDENTIALS)?;

        if !user.verify_password(&params.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.

Image