A full-stack Forma application is an ordinary Rust web application with Forma page rendering. You can create JSON APIs, validate requests, connect to a database, call external services and run your business logic in the same server that serves your pages. Forma supplies the UI and rendering contracts; your application chooses its storage, authentication and service libraries.
This guide extends the published 3.0.0 minimal starter from the installation guide.
Understand the parts#
| Part | Responsibility | Familiar JavaScript comparison |
|---|---|---|
| Rust | Language used to write and compile the backend | JavaScript or TypeScript, rather than Node.js itself |
| Tokio | Async runtime used by the server | Some of the async execution work provided by Node.js |
| Axum | HTTP routes, request extraction and responses | Express or Fastify |
| Tower middleware | Processing around route handlers | Express middleware |
forma-server | Typed page rendering and frontend asset integration | The rendering integration within your server |
Browser fetch | Calls from your Forma frontend to an HTTP endpoint | The same browser Fetch API you already use |
An HTTP client such as reqwest | Outgoing requests from Rust to another API | Server-side fetch or Axios |
These comparisons describe responsibilities, not identical APIs. Axum receives HTTP requests; an HTTP client sends them. Rust compiles to a native executable; it is not a JavaScript runtime. Node.js runs Forma's frontend build tools.
The minimal starter already declares Axum and Tokio in Cargo.toml. Its first
Cargo build downloads them along with the Forma crates. You do not need to
install Axum separately after creating the app, or run another web server beside
it. Rust and Cargo themselves remain prerequisites.
Add a JSON endpoint#
Open the starter's src/main.rs. Add this function outside main:
async fn api_items() -> axum::Json<Vec<&'static str>> {
axum::Json(vec!["Field notebook", "Canvas pouch"])
}In the existing Router::new() chain, add this line before .with_state(state):
.route("/api/items", get(api_items))Keep the existing page and asset routes, page binding and finalize() calls.
get is already imported by the starter. This JSON array uses the starter's
existing dependencies, so there is no manifest change. API routes do not need a
Forma page entry or an FMIR module in admin/build.ts.
After following the installation guide's frontend build, restart the server from the application root:
cargo run --locked --jobs 3In another terminal:
curl --fail http://localhost:3000/api/itemsThe response is 200 OK, has a JSON content type and contains:
["Field notebook", "Canvas pouch"]This first endpoint returns fixed example data. It does not read a database or save edits. From a browser event handler or other browser-owned code, call it with ordinary Fetch:
const response = await fetch("/api/items");
if (!response.ok) throw new Error(`Items request failed: ${response.status}`);
const items = await response.json();Use the returned data to update your Forma signals. A native page handler can instead call the same Rust service directly and pass the result into declared page props; it does not need to make an HTTP request to its own API.
Organize request handling and business logic#
As the application grows, separate routes, services and storage into Rust modules. A typical write follows this path:
Browser form or fetch
→ Axum route and middleware
→ parse and validate the request
→ service applies business rules
→ storage commits the change
→ JSON response or redirect to fresh HTMLAxum extractors handle path parameters, query strings, forms, JSON and shared
application state. For your own typed JSON bodies and responses, a common next
dependency is Serde. Add it from the directory containing Cargo.toml:
cargo add serde@1 --features deriveReview and commit the resulting manifest and lockfile changes. Derive
Deserialize on request types and Serialize on response types. Parsing a
quantity as a number does not establish that the quantity is allowed: enforce
business rules and authorization in the backend before writing data. See
Axum's extractors and
response types.
Forma 3.0 uses explicit HTTP routes for browser-to-server operations. It does
not generate an RPC endpoint from a TypeScript "use server" function.
Ordinary Rust functions and service modules remain available throughout your
backend. You write the HTTP request and response contract explicitly, which adds
plumbing without restricting the backend operations you can implement. Explicit
routes still need application authentication, authorization and validation.
Connect storage and distinguish schemas#
Choose a Rust database driver or library appropriate to your database. For
example, SQLx provides database access,
connection pools and migrations. This is an application dependency, not a
bundled Forma database adapter. Follow that library's version, runtime and TLS
setup instructions when adding it to Cargo.toml.
Create the connection pool during server startup and keep it in your existing
AppState. Handlers access that state with Axum's State extractor. Reuse the
pool across requests; obtain connections or transactions for each operation.
Keep credentials in server configuration, outside browser bundles and page props.
See sharing state in Axum.
Three contracts serve different purposes:
| Contract | What it describes |
|---|---|
| Database schema and migrations | Tables, constraints, indexes and changes to stored data |
| Request and response types | The API's input/output shape, with application validation |
Forma PageProps | The declared data supplied to a rendered page and its islands |
PageProps does not create database tables or authorize a request. Services can
query, update and transact on stored data, then map it into the presentation
values the page needs. Backend Rust logic is not restricted to Forma's native
TSX expression subset.
Add middleware and outgoing API calls#
Axum uses Tower for middleware. Apply layers to the relevant router or group of
routes for authentication, request tracing, body limits or other request policy.
Place authorization before the protected operation; a CORS policy does not
establish a user's permission. Add any optional middleware crates deliberately
and follow Axum's middleware guide.
Keep Forma's complete PageResponse and its response headers when returning
rendered pages.
For calls from Rust to a payment, email or other external API, add a client such
as reqwest. Reuse its client in shared
state, configure timeouts, handle unsuccessful responses and keep API keys on
the server. This is separate from Axum routing and from browser fetch.
Select dependency features and versions compatible with your application's
Rust toolchain, and commit the updated Cargo lockfile.
Follow a complete saved edit#
Field Notes is the runnable next step. Its source and download show Axum routes, typed form input, request checks, conflicts, a service/store layer and a browser Fetch enhancement over ordinary HTML forms. The example verifies that accepted edits survive a server restart.
Field Notes uses a single-process JSON file store and has no user accounts. It demonstrates the application structure; it is not a database or authentication integration. Use its tested write path as a starting point, then consult the libraries you choose for their complete APIs. Continue with application testing and deployment when those integrations are in place.