CtrlK
BlogDocsLog inGet started
Tessl Logo

rocket-project-starter

Scaffold a production-ready Rocket 0.5+ API with Rust 2024 edition, request guards, fairings, managed state, responders, database integration, and typed configuration.

79

Quality

71%

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Passed

No known issues

Optimize this skill with Tessl

npx tessl skill review --optimize ./backend-rust/rocket-project-starter/SKILL.md
SKILL.md
Quality
Evals
Security

Rocket Project Starter

Scaffold a production-ready Rocket 0.5+ API with Rust 2024 edition, request guards, fairings, managed state, responders, database integration, and typed configuration.

Prerequisites

  • Rust >= 1.85 (2024 edition support)
  • Cargo
  • PostgreSQL (or SQLite for development)

Scaffold Command

cargo init <project-name>
cd <project-name>

# Core dependencies
cargo add rocket@0.5 --features json,secrets
cargo add rocket_db_pools --features sqlx_postgres
cargo add serde --features derive
cargo add serde_json
cargo add sqlx --features runtime-tokio,tls-rustls,postgres,uuid,chrono,migrate
cargo add uuid --features v4,serde
cargo add chrono --features serde
cargo add thiserror
cargo add dotenvy

# Optional — Diesel instead of SQLx
# cargo add rocket_sync_db_pools --features diesel_postgres
# cargo add diesel --features postgres,uuid,chrono

# Dev dependencies
cargo add --dev rocket --features local_blocking

Project Structure

src/
  main.rs                  # Rocket launch, mount routes, attach fairings
  config.rs                # Custom config from Rocket.toml / env
  db.rs                    # Database pool (rocket_db_pools)
  errors.rs                # AppError with custom Responder
  routes/
    mod.rs                 # Route mounting
    health.rs              # Health check
    users.rs               # User CRUD routes
  models/
    mod.rs
    user.rs                # User struct, request/response types
  guards/
    mod.rs
    auth.rs                # FromRequest guard for authentication
  fairings/
    mod.rs
    cors.rs                # CORS fairing
    timing.rs              # Request timing fairing
Rocket.toml                # Environment-based configuration
Cargo.toml
.env
.env.example                 # Template for required env vars (commit this)

Key Conventions

  • Routes are functions annotated with #[get], #[post], #[put], #[delete] macros
  • Request guards (FromRequest) validate and extract data before the handler runs — if a guard fails, the handler is never called
  • Fairings are Rocket's middleware — on_request, on_response, on_ignite, on_liftoff hooks
  • Managed state via rocket::State<T> — registered with .manage(value) at launch
  • Responders (impl Responder) control how return types become HTTP responses
  • Configuration via Rocket.toml with [default], [debug], [release] profiles
  • Use rocket_db_pools for async database connection pooling
  • Rust 2024 edition in Cargo.toml: edition = "2024"
  • Rocket uses its own async runtime — do not use #[tokio::main], use #[launch] or #[rocket::main]

Essential Patterns

Application Bootstrap — main.rs

#[macro_use]
extern crate rocket;

use rocket_db_pools::Database;

mod config;
mod db;
mod errors;
mod fairings;
mod guards;
mod models;
mod routes;

use db::Db;

#[launch]
fn rocket() -> _ {
    dotenvy::dotenv().ok();

    rocket::build()
        .attach(Db::init())
        .attach(fairings::cors::Cors)
        .attach(fairings::timing::RequestTimer)
        .mount("/api", routes::all_routes())
}

Database Pool — db.rs

use rocket_db_pools::{sqlx, Database};

#[derive(Database)]
#[database("app_db")]
pub struct Db(sqlx::PgPool);

Configuration — Rocket.toml

[default]
address = "0.0.0.0"
port = 8080
secret_key = "generate-a-256-bit-base64-key-for-production"

[default.databases.app_db]
url = "postgres://user:pass@localhost:5432/myapp"
max_connections = 5
connect_timeout = 5

[debug]
log_level = "debug"

[release]
log_level = "normal"

Route Mounting — routes/mod.rs

use rocket::Route;

mod health;
mod users;

pub fn all_routes() -> Vec<Route> {
    let mut routes = Vec::new();
    routes.extend(health::routes());
    routes.extend(users::routes());
    routes
}

Handlers — routes/users.rs

use rocket::serde::json::Json;
use rocket::http::Status;
use rocket::response::status;
use rocket_db_pools::Connection;
use uuid::Uuid;

use crate::db::Db;
use crate::errors::AppError;
use crate::guards::auth::AuthenticatedUser;
use crate::models::user::{CreateUserRequest, User};

pub fn routes() -> Vec<rocket::Route> {
    routes![create_user, get_user, list_users, update_user, delete_user]
}

#[post("/users", data = "<body>")]
async fn create_user(
    mut db: Connection<Db>,
    body: Json<CreateUserRequest>,
) -> Result<status::Created<Json<User>>, AppError> {
    let user = sqlx::query_as!(
        User,
        r#"INSERT INTO users (id, email, name)
           VALUES ($1, $2, $3)
           RETURNING id, email, name, created_at, updated_at"#,
        Uuid::new_v4(),
        body.email,
        body.name,
    )
    .fetch_one(&mut **db)
    .await?;

    let location = format!("/api/users/{}", user.id);
    Ok(status::Created::new(location).body(Json(user)))
}

#[get("/users/<id>")]
async fn get_user(
    mut db: Connection<Db>,
    _auth: AuthenticatedUser,
    id: &str,
) -> Result<Json<User>, AppError> {
    let id = Uuid::parse_str(id).map_err(|_| AppError::BadRequest("Invalid UUID".into()))?;

    let user = sqlx::query_as!(
        User,
        "SELECT id, email, name, created_at, updated_at FROM users WHERE id = $1",
        id,
    )
    .fetch_optional(&mut **db)
    .await?
    .ok_or(AppError::NotFound(format!("User {id} not found")))?;

    Ok(Json(user))
}

#[get("/users?<limit>&<offset>")]
async fn list_users(
    mut db: Connection<Db>,
    limit: Option<i64>,
    offset: Option<i64>,
) -> Result<Json<Vec<User>>, AppError> {
    let limit = limit.unwrap_or(20).min(100);
    let offset = offset.unwrap_or(0);

    let users = sqlx::query_as!(
        User,
        "SELECT id, email, name, created_at, updated_at FROM users LIMIT $1 OFFSET $2",
        limit,
        offset,
    )
    .fetch_all(&mut **db)
    .await?;

    Ok(Json(users))
}

#[put("/users/<id>", data = "<body>")]
async fn update_user(
    mut db: Connection<Db>,
    _auth: AuthenticatedUser,
    id: &str,
    body: Json<CreateUserRequest>,
) -> Result<Json<User>, AppError> {
    let id = Uuid::parse_str(id).map_err(|_| AppError::BadRequest("Invalid UUID".into()))?;

    let user = sqlx::query_as!(
        User,
        r#"UPDATE users SET email = $1, name = $2, updated_at = NOW()
           WHERE id = $3
           RETURNING id, email, name, created_at, updated_at"#,
        body.email,
        body.name,
        id,
    )
    .fetch_optional(&mut **db)
    .await?
    .ok_or(AppError::NotFound(format!("User {id} not found")))?;

    Ok(Json(user))
}

#[delete("/users/<id>")]
async fn delete_user(
    mut db: Connection<Db>,
    _auth: AuthenticatedUser,
    id: &str,
) -> Result<Status, AppError> {
    let id = Uuid::parse_str(id).map_err(|_| AppError::BadRequest("Invalid UUID".into()))?;

    let rows = sqlx::query!("DELETE FROM users WHERE id = $1", id)
        .execute(&mut **db)
        .await?
        .rows_affected();

    if rows == 0 {
        return Err(AppError::NotFound(format!("User {id} not found")));
    }

    Ok(Status::NoContent)
}

Model — models/user.rs

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use uuid::Uuid;

#[derive(Debug, Serialize, FromRow)]
pub struct User {
    pub id: Uuid,
    pub email: String,
    pub name: String,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
}

#[derive(Debug, Deserialize)]
pub struct CreateUserRequest {
    pub email: String,
    pub name: String,
}

Error Handling with Custom Responder — errors.rs

use rocket::http::Status;
use rocket::response::{self, Responder};
use rocket::serde::json::Json;
use rocket::Request;

#[derive(Debug, thiserror::Error)]
pub enum AppError {
    #[error("Not found: {0}")]
    NotFound(String),

    #[error("Bad request: {0}")]
    BadRequest(String),

    #[error("Unauthorized")]
    Unauthorized,

    #[error("Database error: {0}")]
    Database(#[from] sqlx::Error),
}

impl<'r> Responder<'r, 'static> for AppError {
    fn respond_to(self, req: &'r Request<'_>) -> response::Result<'static> {
        let (status, message) = match &self {
            AppError::NotFound(msg) => (Status::NotFound, msg.clone()),
            AppError::BadRequest(msg) => (Status::BadRequest, msg.clone()),
            AppError::Unauthorized => (Status::Unauthorized, "Unauthorized".into()),
            AppError::Database(e) => {
                error!("Database error: {e}");
                (Status::InternalServerError, "Internal server error".into())
            }
        };

        let body = Json(serde_json::json!({ "error": message }));
        response::Response::build_from(body.respond_to(req)?)
            .status(status)
            .ok()
    }
}

Request Guard — guards/auth.rs

use rocket::http::Status;
use rocket::request::{FromRequest, Outcome, Request};
use uuid::Uuid;

pub struct AuthenticatedUser {
    pub user_id: Uuid,
}

#[rocket::async_trait]
impl<'r> FromRequest<'r> for AuthenticatedUser {
    type Error = &'static str;

    async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
        let auth_header = req.headers().get_one("Authorization");

        match auth_header {
            Some(header) if header.starts_with("Bearer ") => {
                let token = &header[7..];
                // Validate token and extract user_id
                match validate_token(token) {
                    Ok(user_id) => Outcome::Success(AuthenticatedUser { user_id }),
                    Err(_) => Outcome::Error((Status::Unauthorized, "Invalid token")),
                }
            }
            _ => Outcome::Error((Status::Unauthorized, "Missing Authorization header")),
        }
    }
}

fn validate_token(token: &str) -> Result<Uuid, ()> {
    // Replace with actual JWT validation
    let _ = token;
    Err(())
}

CORS Fairing — fairings/cors.rs

use rocket::fairing::{Fairing, Info, Kind};
use rocket::http::Header;
use rocket::{Request, Response};

pub struct Cors;

#[rocket::async_trait]
impl Fairing for Cors {
    fn info(&self) -> Info {
        Info {
            name: "CORS Headers",
            kind: Kind::Response,
        }
    }

    async fn on_response<'r>(&self, _req: &'r Request<'_>, res: &mut Response<'r>) {
        res.set_header(Header::new("Access-Control-Allow-Origin", "*"));
        res.set_header(Header::new(
            "Access-Control-Allow-Methods",
            "GET, POST, PUT, DELETE, OPTIONS",
        ));
        res.set_header(Header::new(
            "Access-Control-Allow-Headers",
            "Content-Type, Authorization",
        ));
    }
}

Request Timing Fairing — fairings/timing.rs

use rocket::fairing::{Fairing, Info, Kind};
use rocket::{Data, Request, Response};
use std::time::Instant;

pub struct RequestTimer;

#[rocket::async_trait]
impl Fairing for RequestTimer {
    fn info(&self) -> Info {
        Info {
            name: "Request Timer",
            kind: Kind::Request | Kind::Response,
        }
    }

    async fn on_request(&self, req: &mut Request<'_>, _data: &mut Data<'_>) {
        req.local_cache(|| Instant::now());
    }

    async fn on_response<'r>(&self, req: &'r Request<'_>, res: &mut Response<'r>) {
        let start = req.local_cache(|| Instant::now());
        let duration = start.elapsed();
        info!("{} {} — {}ms", req.method(), req.uri(), duration.as_millis());
        res.set_header(rocket::http::Header::new(
            "X-Response-Time",
            format!("{}ms", duration.as_millis()),
        ));
    }
}

First Steps After Scaffold

  1. Copy .env.example to .env and configure any required env vars
  2. Configure Rocket.toml with your database URL in [default.databases.app_db]
  3. Run cargo build to fetch and compile all dependencies
  4. Ensure PostgreSQL is running and create the database: createdb myapp
  5. Start the dev server: cargo watch -x run
  6. Verify the endpoint: curl http://localhost:8080/api/health

Common Commands

# Development (Rocket auto-reloads on change with cargo-watch)
cargo install cargo-watch
cargo watch -x run

# Run
cargo run

# Build release
cargo build --release

# Run tests
cargo test

# Lint
cargo clippy -- -D warnings

# Format
cargo fmt

# Run with specific profile
ROCKET_PROFILE=release cargo run

# Override config via env
ROCKET_PORT=9090 cargo run
ROCKET_DATABASES='{app_db={url="postgres://..."}}' cargo run

Integration Notes

  • Database: rocket_db_pools for async pools (SQLx, deadpool). rocket_sync_db_pools for sync ORMs (Diesel). Both use Rocket.toml [databases] config.
  • Auth: Request guards are the idiomatic way to enforce auth. Add AuthenticatedUser parameter to any handler that requires authentication — if the guard fails, Rocket returns the error automatically.
  • Managed State: Use .manage(value) at launch for singletons (config objects, HTTP clients, caches). Access via &State<T> in handlers.
  • Testing: Use rocket::local::asynchronous::Client (or blocking::Client) to create a test client. No server port needed — tests run in-process.
  • Templates: Use rocket_dyn_templates with Handlebars or Tera for server-side rendering.
  • Static Files: Use rocket::fs::FileServer to serve static assets from a directory.
  • Catchers: Define #[catch(404)] and #[catch(500)] functions for custom error pages. Register with .register("/", catchers![...]).
  • Docker: Multi-stage build — rust:1.85-slim for build, debian:bookworm-slim for runtime. Copy Rocket.toml alongside the binary.
Repository
achreftlili/deep-dev-skills
Last updated
Created

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.