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:

cargo build

If the build succeeds, go ahead and generate the openapi.json spec in the current directory:

cargo run -- . 

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:

cd ui
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
yarn
pnpm
bun
deno
npm install 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&nbsp;
        <a href="/entities" style={{ color: 'var(--color-blue-600)' }}>
          here
        </a>
        &nbsp;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:

cargo run

Run the frontend:

pnpm dev

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.