Skip to content
forma.
v3.0
☰
Browse documentation
Start here
Build interfaces
Native rendering
Tooling
Security and operations
Architecture
Application guides
API reference

Docs/Application guides

FORMA 3.0

Build your backend

Add Axum APIs, middleware, database access and service logic alongside Forma rendering.

View source ↗Markdown ↓Site guide · Forma 3.0
On this page

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#

PartResponsibilityFamiliar JavaScript comparison
RustLanguage used to write and compile the backendJavaScript or TypeScript, rather than Node.js itself
TokioAsync runtime used by the serverSome of the async execution work provided by Node.js
AxumHTTP routes, request extraction and responsesExpress or Fastify
Tower middlewareProcessing around route handlersExpress middleware
forma-serverTyped page rendering and frontend asset integrationThe rendering integration within your server
Browser fetchCalls from your Forma frontend to an HTTP endpointThe same browser Fetch API you already use
An HTTP client such as reqwestOutgoing requests from Rust to another APIServer-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:

rust
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):

rust
.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:

sh
cargo run --locked --jobs 3

In another terminal:

sh
curl --fail http://localhost:3000/api/items

The response is 200 OK, has a JSON content type and contains:

json
["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:

js
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:

text
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 HTML

Axum 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:

sh
cargo add serde@1 --features derive

Review 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:

ContractWhat it describes
Database schema and migrationsTables, constraints, indexes and changes to stored data
Request and response typesThe API's input/output shape, with application validation
Forma PagePropsThe 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.

Find your next answer.

Search Forma 3.0 documentation, guides and examples.