Quick Start
Getting setup to use Stately is quite simple. Follow the steps below and for a more complete walkthrough guide, refer to the basic demo Tasks.
Project Structure
A minimal Stately project might have any of the following structures. Of course this is not necessary, but it is a good starting point.
Rust Backend
Package structure:
my-app/
├── Cargo.toml # Rust project
├── src/
│ ├── bin
│ │ └── api.rs # Codegen entry point
│ ├── main.rs # Main entry point
│ ├── state.rs # Entity definitions
│ ├── openapi.rs # Openapi definitions
│ └── api.rs # API configuration
│
└── ui/ # Typescript frontend
With workspace:
my-app/
├── Cargo.toml # Rust workspace
├── crates/
│ ├── my-app/ # Main app bin
│ │ ├── Cargo.toml
│ │ └── src/
│ │ └── main.rs # Cli entry point + codegen entry point
│ └── my-app-lib/ # Main app lib
│ ├── Cargo.toml
│ └── src/
│ ├── lib.rs # Entry point
│ ├── state.rs # Entity definitions
│ ├── openapi.rs # Openapi definitions
│ └── api.rs # API configuration
│
└── ui/ # Typescript frontend
Typescript frontend
my-app/
├── Cargo.toml
├── ... # Rust backend
├── openapi.json # Generated OpenAPI spec from backend codegen entry point
└── ui/ # Typescript frontend
├── package.json
├── tsconfig.json
└── src/
├── App.tsx
├── index.css # TailwindCSS v4 entrypoint
├── lib/
│ └── stately.ts # Stately integration and bootstrap
└── generated/ # Generated via @statelyjs/stately/codegen
├── types.ts # openapi-typescript generated types
└── schemas.ts # Stately parsed schema nodes
Verifying Installation
Install Dependencies
If you haven't already done so, refer to Installation to install the necessary packages and dependencies. Be sure to install everything needed for the "axum" and "openapi" features as well.
Backend
Entrypoint main.rs
Create a simple entity, state shape, and basic axum api to verify your Rust setup:
use std::net::SocketAddr;
use axum::Router;
use serde::{Deserialize, Serialize};
use tower_http::cors::{Any, CorsLayer};
/// Example entity
///
/// Doc comments appear as descriptions in the UI. Only the first line is used as the description,
/// any additional doc comments will not be displayed on the UI.
#[stately::entity]
pub struct Example {
/// Example name
pub name: String,
/// Example count
pub count: usize,
}
#[stately::state(openapi)]
#[derive(Debug, Clone)]
pub struct State {
examples: Example
}
#[stately::axum_api(State, openapi(components = [Example])))]
#[derive(Clone)]
pub struct ApiState {}
#[tokio::main]
async fn main() {
// ...Binary being run in codegen mode
if let Some(output_dir) = std::env::args().nth(1) {
let path = stately::codegen::generate_openapi::<ApiState>(&output_dir).unwrap();
println!("OpenAPI spec written to {}", path.display());
std::process::exit(0);
};
// ...Binary being run in api mode
// Initialize state
let mut state = State::new();
// Create entity
let entity_id =
state.create_entity(Entity::Example(Example { name: "test".to_string(), count: 0 }));
println!("Stately entity created successfully: {entity_id}");
// Create and initialize api state
let api_state = ApiState::new(state);
// Create api routes, nesting stately under `/api/entity`
let app = Router::new()
.nest("/api/entity", ApiState::router::<ApiState>(api_state.clone()))
.layer(CorsLayer::new().allow_headers(Any).allow_methods(Any).allow_origin(Any))
.with_state(api_state);
// Start axum server
let addr = SocketAddr::from(([0, 0, 0, 0], 4000));
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
eprintln!("Server running at http://{addr}");
axum::serve(listener, app).await.unwrap();
println!("Server exiting!");
std::process::exit(0);
}
Build and Generate OpenAPI
Run cargo build to build the rust application:
If the build succeeds, go ahead and generate the openapi.json spec in the current directory:
If the spec was generated successfully, you should see the openapi.json file in the root of the project. Time to integrate the frontend!
Frontend
Let's now build the frontend application. Change into the ui directory if you haven't already:
DETAILS
This demo uses Vite, React 19, and React Router. If you don't know how to setup a project with these tools, a link to the source code of the quick-start is provided at the end of this guide.
TypeScript Codegen via OpenAPI
Stately includes a CLI for generating TypeScript types from your OpenAPI spec:
# Generate types and schemas,
pnpm exec stately generate ../openapi.json -o ./src/generated
If using a plugin configuration file, run the command with the configuration option (this demo does not):
# Provide path to optional plugin configuration, not used in this demo
pnpm exec stately generate ../openapi.json -o ./src/generated -c stately.plugins.ts
This creates two files in ui/src/generated:
types.ts - TypeScript types from OpenAPI components
schemas.ts - Parsed schema definitions for runtime form generation
Stately Integration
Now that the types have been generated, bootstrap and integrate your frontend application with stately:
mkdir -p src/lib
touch src/lib/stately.ts
Add the following to the file just created, ui/src/lib/stately.ts:
// Application imports
import { statelyUi, statelyUiProvider, useStatelyUi } from '@statelyjs/stately';
import { type DefineConfig, type Schemas, stately } from '@statelyjs/stately/schema';
import createClient from 'openapi-fetch';
// Generated imports
import openapiSpec from '../../../openapi.json';
import { PARSED_SCHEMAS, type ParsedSchema } from '../generated/schemas';
import type { components, operations, paths } from '../generated/types';
// Create derived stately schema
type AppSchemas = Schemas<
DefineConfig<components, paths, operations, ParsedSchema>,
readonly [/** Plugin schema factories */]
>;
// Create stately runtime
export const runtime = statelyUi<AppSchemas, readonly [/** Plugin UI factories */]>({
client: createClient<paths>({ baseUrl: 'http://localhost:5555/api/entity' }),
// Pass in derived stately schema
schema: stately<AppSchemas>(openapiSpec, PARSED_SCHEMAS),
});
// Create application's context provider
export const StatelyProvider = statelyUiProvider<AppSchemas, readonly []>();
export const useStately = useStatelyUi<AppSchemas, readonly []>;
App.tsx and Page Routing
Stately works with any framework, ie @tanstack/react-router, next.js, react-router-dom, etc.
For this demo, we will use react-router-dom.
Ensure it is installed:
npm install react-router-dom
yarn add react-router-dom
pnpm add react-router-dom
deno add npm:react-router-dom
Finally, let's setup the app entrypoint, using react-router-dom to route our application:
import * as EntityPages from '@statelyjs/stately/core/pages';
import { Layout } from '@statelyjs/stately/layout';
import { Gem } from 'lucide-react';
import { Navigate, Route, Routes, useParams } from 'react-router-dom';
const useRequiredParams = <T extends Record<string, unknown>>() => useParams() as T;
function App() {
return (
<Layout.Root
sidebarProps={{
collapsible: 'icon',
logo: <Gem />,
logoName: 'Quick-start',
variant: 'floating'
}}
>
<Routes>
{/* Home */}
<Route element={<Home />} index path="/" />
{/* Entity routes */}
<Route element={<Entities />} path="/entities/*" />
{/* Fallback */}
<Route element={<Navigate replace to="/" />} path="*" />
</Routes>
</Layout.Root>
);
}
// Simple Home component
function Home() {
return (
<Layout.Page title="Welcome to the Quick-start Demo!">
<p>
Click
<a href="/entities" style={{ color: 'var(--color-blue-600)' }}>
here
</a>
to explore entities.
</p>
</Layout.Page>
);
}
// Entrypoint into entity configurations
function Entities() {
return (
<Routes>
<Route element={<EntityPages.EntitiesIndexPage />} index path="/" />
<Route element={<EntityType />} path="/:type/*" />
</Routes>
);
}
// Entrypoint into an entity type
function EntityType() {
const { type } = useRequiredParams<{ type: string }>();
return (
<Routes>
<Route element={<EntityPages.EntityTypeListPage entity={type} />} index path="/" />
<Route element={<EntityPages.EntityNewPage entity={type} />} path="/new" />
<Route element={<Entity entity={type} />} path="/:id/*" />
</Routes>
);
}
// Entrypoint into an instance of an entity
function Entity({ entity }: React.ComponentProps<typeof EntityPages.EntityNewPage>) {
const { id } = useRequiredParams<{ id: string }>();
return (
<Routes>
<Route element={<EntityPages.EntityDetailsPage entity={entity} id={id} />} index path="/" />
<Route element={<EntityPages.EntityEditPage entity={entity} id={id} />} path="/edit" />
</Routes>
);
}
export default App;
Styles and Tailwind
Stately requires Tailwind CSS v4. Create src/index.css and configure it as follows:
/* Import Tailwind */
@import "tailwindcss";
/* Import Stately theme tokens and base styles */
@import "@statelyjs/stately/styles.css";
/* Scan your app source for Tailwind classes */
@source ".";
/*
* Scan all Stately packages for utility classes.
* Tailwind excludes node_modules by default, so an explicit @source is required.
* This single directive covers stately, ui, and any @statelyjs plugins.
*/
@source "./node_modules/@statelyjs";
INFO
Why @source directives? Tailwind v4 scans your source files to detect which utility classes are used and includes only those in the final CSS bundle. Since node_modules is excluded by default, you must explicitly tell Tailwind to scan Stately packages. This ensures all component styles are included while keeping your bundle optimized.
Stately's UI Context Provider
Much of the UI works via standard React context providers. Setup is simple, just wrap your root in the context provider StatelyProvider you created in previous steps and pass in the Stately runtime:
import './index.css';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import { runtime, StatelyProvider } from './lib/stately';
const queryClient = new QueryClient();
// biome-ignore lint/style/noNonNullAssertion: ''
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<BrowserRouter>
<QueryClientProvider client={queryClient}>
<StatelyProvider runtime={runtime}>
<App />
</StatelyProvider>
</QueryClientProvider>
</BrowserRouter>
</React.StrictMode>,
);
Recap
That's it! You are setup and ready to go. Depending on whether you used react-router-dom or another routing library, whether you prefer Vite or Webpack, there are a bit more details left out, but they are all standard web dev. At this point, you can run the application and see it work.
Run the backend:
Run the frontend:
Source Code
All of the source code for this simple quick-start are available in the stately demos folder, this quick start can be found in the Quick-start demo's source.
Next Steps
To see a similar demo with a core concepts expanded, review the Tasks demo. To learn a bit more about why stately was built the way it was, head over to Why Stately.