backend-rust/rocket-project-starter/SKILL.md
Scaffold a production-ready Rocket 0.5+ API with Rust 2024 edition, request guards, fairings, managed state, responders, database integration, and typed configuration.
npx skillsauth add achreftlili/deep-dev-skills rocket-project-starterInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Scaffold 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 [email protected] --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
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)
#[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 run
rocket_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.testing
Set up Vitest 2.x with TypeScript for unit and component testing using test/describe/it, vi.fn/vi.mock/vi.spyOn, component testing with Testing Library, coverage (v8/istanbul), workspace config, and snapshot testing.
testing
Set up pytest 8.x with Python for unit and integration testing using fixtures (scope, autouse, parametrize), async tests (pytest-asyncio), mocking (unittest.mock, pytest-mock), coverage (pytest-cov), conftest.py patterns, and markers.
testing
Set up Playwright 1.49+ with TypeScript for E2E testing using page object model, fixtures, test.describe/test blocks, assertions, selectors, network mocking, CI configuration, and trace viewer.
testing
Set up Jest 30+ with TypeScript for unit tests, integration tests, mocking (jest.fn, jest.mock, jest.spyOn), coverage configuration, custom matchers, snapshot testing, and setup/teardown patterns.