This document provides technical context for AI coding agents working on the Rejoice framework.
src/
├── bin/
│ ├── main.rs # CLI entry point (uses clap)
│ └── commands/
│ ├── mod.rs # Command exports
│ ├── init.rs # `rejoice init` - project scaffolding
│ ├── dev.rs # `rejoice dev` - dev server with HMR
│ ├── build.rs # `rejoice build` - production builds
│ ├── migrate.rs # `rejoice migrate` - database migrations
│ ├── boilerplate.rs # Auto-generates route/layout boilerplate
│ ├── islands.rs # Generates client/islands.tsx registry
│ └── style.rs # Terminal output helpers
├── assets/
│ └── live_reload.js # Client-side HMR script (injected into HTML)
├── app.rs # App struct, middleware, server setup
├── codegen.rs # Build-time route generation
├── db.rs # SQLite pool config and exports
├── env.rs # Re-exports dotenvy_macro::dotenv as env!
├── island.rs # Island macro for SolidJS components
├── request.rs # Req type for incoming request data
├── response.rs # Res type for building responses
└── lib.rs # Public API exports
The CLI uses clap with derive macros. Defined in src/bin/main.rs.
Creates a new project. Implementation in src/bin/commands/init.rs.
Without --with-db:
- Basic project with
App::new()androutes!() - Routes receive
req: Req, res: Res - No database files
With --with-db:
- Creates
.envwithDATABASE_URLand empty.dbfile - Generates
AppStatestruct with db pool inmain.rs - Uses
App::with_state()androutes!(AppState) - Routes receive
state: AppState, req: Req, res: Res
IMPORTANT: When changing the framework's public API, imports, or patterns, update the generated templates in init.rs to match.
Starts the dev server with:
- Cargo watch for Rust recompilation
- Vite watch for client assets (via Bun)
- WebSocket-based live reload
- Auto-generates boilerplate for new route/layout files
Builds the project for deployment. Implementation in src/bin/commands/build.rs.
Steps performed:
- Install dependencies with Bun (if
node_modules/missing andclient/exists) - Generate islands registry (if
client/exists) - Build client assets with Vite (if
client/exists) - Build Rust binary with Cargo
Flags:
--release- Build with optimizations, prints deployment instructions
Output locations:
- Binary:
target/debug/<name>ortarget/release/<name> - Client assets:
dist/islands.js,dist/styles.css
Database migrations via sqlx-cli. Implementation in src/bin/commands/migrate.rs.
Subcommands:
rejoice migrate add <name>- Create a new reversible migration (up.sql + down.sql)rejoice migrate up- Apply pending migrationsrejoice migrate revert- Revert the last migrationrejoice migrate status- Show migration status
If sqlx-cli is not installed, the command will offer to install it automatically.
The codegen.rs module runs at build time via the user's build.rs:
fn main() {
rejoice::codegen::generate_routes();
}src/routes.rs- Module declarations for rust-analyzer support$OUT_DIR/routes_generated.rs- Actual router code, included viaroutes!()macro
Scans src/routes/ recursively:
index.rs→/or/parentabout.rs→/about[id].rs→/:id(dynamic segment)[id]/→ Directory with dynamic segment (e.g.,users/[id]/posts/→/users/:id/posts)layout.rs→ Wrapper for sibling/child routes
Dynamic segments can appear anywhere in the path, including directories. Multiple dynamic segments are supported (e.g., /users/:user_id/posts/:post_id).
Route files export functions named after HTTP methods:
get→ GET request handlerpost→ POST request handlerput→ PUT request handlerdelete→ DELETE request handlerpatch→ PATCH request handler
A single route file can export multiple handlers for different methods.
For routes with layouts, generates wrapper functions that:
- Extract state via Axum's
Stateextractor (internally) - Extract
Resfrom request parts andReqfrom the full request (including body) - Call the handler function with
(state, req, res)or(req, res) - If the response is HTML, wrap with layouts (innermost to outermost)
- If the response is not HTML (redirect, JSON, etc.), return it directly without layout wrapping
Note: Req is extracted last because it consumes the request body.
The __RejoiceState type alias is defined by the routes!() macro:
routes!()→type __RejoiceState = ();routes!(AppState)→type __RejoiceState = AppState;
pub fn create_router() -> axum::Router<__RejoiceState> {
axum::Router::new()
.route("/", axum::routing::get(wrapper_index))
// ... more routes
}The Req type provides access to request data including body:
pub struct Req {
pub headers: HeaderMap, // HTTP headers
pub cookies: Cookies, // Parsed cookies
pub method: Method, // GET, POST, etc.
pub uri: Uri, // Request URI
pub body: Body, // Request body (for POST, PUT, etc.)
}
// Reading request data
let auth = req.headers.get("Authorization");
let session = req.cookies.get("session_id");
// Parsing POST body
let form = req.body.as_form::<MyForm>()?;
let json = req.body.as_json::<MyData>()?;The Res type uses interior mutability for building responses.
Mutators (return &Res for chaining):
set_cookie(name, value)- Set a cookieset_cookie_with_options(...)- Set cookie with path, max_age, etc.delete_cookie(name)- Delete a cookieset_header(name, value)- Set a response headerset_status(StatusCode)- Override status code
Finalizers (take &self, return owned Res - chainable from mutators):
html(Markup)- HTML response (200, text/html)json(&impl Serialize)- JSON response (200, application/json)redirect(url)- 302 redirectredirect_permanent(url)- 301 redirectraw(impl Into<Vec<u8>>)- Raw bytes
Example usage:
pub async fn get(state: AppState, req: Req, res: Res) -> Res {
// Read cookies
let session = req.cookies.get("session");
if session.is_none() {
// Redirect (bypasses layout wrapping)
return res.redirect("/login");
}
// Set cookies and return HTML
res.set_cookie("last_visit", "2025-01-01")
.set_header("X-Custom", "value")
.html(html! {
h1 { "Dashboard" }
})
}
// API endpoint returning JSON
pub async fn get(state: AppState, req: Req, res: Res) -> Res {
let users = get_users(&state.db).await;
res.json(&users)
}Error helpers:
res.bad_request("Invalid input") // 400
res.unauthorized("Please log in") // 401
res.forbidden("Access denied") // 403
res.not_found("Page not found") // 404
res.internal_error("Server error") // 500
## App and State
### Stateless apps
```rust
let app = App::new(8080, create_router());
let app = App::with_state(8080, create_router(), state);App::with_state() is generic over any S: Clone + Send + Sync + 'static. The state is attached to the router via Axum's .with_state() before serving.
Routes and layouts receive state as a plain value (not wrapped in State):
// Stateless
pub async fn get(req: Req, res: Res) -> Res { ... }
pub async fn post(req: Req, res: Res) -> Res { ... }
pub async fn layout(req: Req, res: Res, children: Children) -> Res { ... }
// Stateful
pub async fn get(state: AppState, req: Req, res: Res) -> Res { ... }
pub async fn post(state: AppState, req: Req, res: Res) -> Res { ... }
pub async fn layout(state: AppState, req: Req, res: Res, children: Children) -> Res { ... }Note: The codegen handles Axum's State extraction internally; user code receives the unwrapped state value.
Feature-gated: The database module requires the sqlite feature flag.
# In user's Cargo.toml
rejoice = { version = "...", features = ["sqlite"] }Exports in src/db.rs (only available with sqlite feature):
Pool,Sqlite- sqlx typesquery,query_as,query_scalar- sqlx query functions/macrosFromRow- Derive macro for mapping query results to structsPoolConfig,create_pool- Pool creation helpers
Users access via rejoice::db::*.
When rejoice init --with-db is used, the generated Cargo.toml automatically includes the sqlite feature.
- User creates TSX component in
client/ComponentName.tsx - User uses
island!(ComponentName, { props })macro in Rust - The macro generates a
<div data-island="ComponentName" data-props='{"props": ...}'> - Vite builds
client/islands.tsx(auto-generated) which registers all components - Client-side JS finds
[data-island]elements and hydrates them with SolidJS
Defined in src/island.rs. Generates:
- Wrapper div with
data-islandattribute (component name) data-propsattribute with JSON-serialized props (HTML-escaped)
src/bin/commands/islands.rs contains generate_islands_registry() which:
- Scans
client/for.tsxand.jsxfiles (excludingislands.tsxitself) - Generates
client/islands.tsxwith imports and a registry object - Includes hydration code that queries
[data-island]elements and renders SolidJS components - Exposes
window.__hydrateIslands()for re-hydration after HMR
This runs automatically during rejoice dev on startup and when client files change.
dev.rsstarts a WebSocket server on port 3001 at/__reloadassets/live_reload.jsis injected into HTML responses (via middleware inapp.rs)- File watchers detect changes to Rust or client files
- On change: rebuild triggered, WebSocket sends reload message
- Client receives message and reloads
"full"- Client JS changed; triggers fulllocation.reload()"reload"- Rust changed; fetches new HTML, swapsdocument.body, re-hydrates islands viawindow.__hydrateIslands()
ScriptInjectionMiddleware in app.rs:
- Checks if response is HTML
- Injects
<script>before</body>for islands and live reload - Injects
<link>in</head>for styles
The public/ directory serves static files at the root URL path:
public/logo.png→/logo.pngpublic/images/hero.jpg→/images/hero.jpgpublic/favicon.ico→/favicon.ico
Implemented in app.rs using fallback_service(ServeDir::new("public")), so defined routes take precedence over static files.
The public/ directory is watched during rejoice dev and triggers a reload when files change.
Configured in the generated vite.config.ts with @tailwindcss/vite plugin.
client/styles.css contains:
@import "tailwindcss";
@source "../src/**/*.rs";
@source "./**/*.tsx";This tells Tailwind to scan Rust and TSX files for class names.
From src/lib.rs:
Root level:
App- Server structReq- Incoming request data (headers, cookies, method, uri)Res- Response builder withset_*mutators and finalizersChildren- Type alias for layout children (Markup)Path- Axum path extractor for dynamic routeshtml!,Markup,DOCTYPE,PreEscaped- Maud re-exports (flattened)json- serde_json::json macroisland!,island_fn- Island supportroutes!- Include generated routes
Prelude module:
use rejoice::prelude::*;
// Brings in: App, Req, Res, Children, Path, html, Markup, DOCTYPE, PreEscaped, json, islandFeature-gated:
db::*- SQLite support (requiressqlitefeature)
Internal (doc-hidden):
State,Router,routing- Used by generated code
IMPORTANT: All dependencies in Cargo.toml must use exact versions (e.g., "1.0.148" not "1"). When adding or updating dependencies, always pin to a specific patch version.
When modifying the framework:
- Changing public API or imports → Update
init.rstemplates - Changing route/layout signatures → Update
codegen.rswrapper generation ANDinit.rs - Adding new exports → Update
lib.rsand this document - Changing CLI commands → Update clap definitions in
main.rs - Changing generated project structure → Update
init.rsstep count and file generation - Any significant changes → Update this
AGENTS.mdfile - ANY change to the framework → Update
llms.txtandllms-full.txtto reflect the change. These files are the comprehensive user-facing documentation for AI agents building apps with Rejoice (following the llmstxt.org spec). They MUST stay perfectly in sync with the actual framework behavior. When in doubt, update them. - ANY change to the framework → Update
/docsto reflect the change. This is the documentation website for Rejoice, and MUST stay perfectly in sync with the actual framework behavior. When in doubt, update it. - ANY change to the framework → Update
README.mdif the change affects user-facing features, API usage examples, or getting started instructions. The README is the first thing users see, so it must accurately reflect how the framework works.