Scaffold a production-ready Rocket 0.5+ API with Rust 2024 edition, request guards, fairings, managed state, responders, database integration, and typed configuration.
79
71%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Passed
No known issues
Optimize this skill with Tessl
npx tessl skill review --optimize ./backend-rust/rocket-project-starter/SKILL.mdScaffold a production-ready Rocket 0.5+ API with Rust 2024 edition, request guards, fairings, managed state, responders, database integration, and typed configuration.
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_blockingsrc/
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)#[get], #[post], #[put], #[delete] macrosFromRequest) validate and extract data before the handler runs — if a guard fails, the handler is never calledon_request, on_response, on_ignite, on_liftoff hooksrocket::State<T> — registered with .manage(value) at launchimpl Responder) control how return types become HTTP responsesRocket.toml with [default], [debug], [release] profilesrocket_db_pools for async database connection poolingCargo.toml: edition = "2024"#[tokio::main], use #[launch] or #[rocket::main]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())
}db.rsuse rocket_db_pools::{sqlx, Database};
#[derive(Database)]
#[database("app_db")]
pub struct Db(sqlx::PgPool);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"routes/mod.rsuse 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
}routes/users.rsuse 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)
}models/user.rsuse 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,
}errors.rsuse 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()
}
}guards/auth.rsuse 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(())
}fairings/cors.rsuse 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",
));
}
}fairings/timing.rsuse 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()),
));
}
}.env.example to .env and configure any required env varsRocket.toml with your database URL in [default.databases.app_db]cargo build to fetch and compile all dependenciescreatedb myappcargo watch -x runcurl http://localhost:8080/api/health# 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 runrocket_db_pools for async pools (SQLx, deadpool). rocket_sync_db_pools for sync ORMs (Diesel). Both use Rocket.toml [databases] config.AuthenticatedUser parameter to any handler that requires authentication — if the guard fails, Rocket returns the error automatically..manage(value) at launch for singletons (config objects, HTTP clients, caches). Access via &State<T> in handlers.rocket::local::asynchronous::Client (or blocking::Client) to create a test client. No server port needed — tests run in-process.rocket_dyn_templates with Handlebars or Tera for server-side rendering.rocket::fs::FileServer to serve static assets from a directory.#[catch(404)] and #[catch(500)] functions for custom error pages. Register with .register("/", catchers![...]).rust:1.85-slim for build, debian:bookworm-slim for runtime. Copy Rocket.toml alongside the binary.181fcbc
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.