How to Use App Context for Sharing Values Across Requests in Topcoat

In Topcoat, you can share global state across all requests by registering values with Router::builder().app_context(value) and retrieving them in handlers using app_context::<T>(cx), which provides thread-safe, type-keyed access to resources like database pools and configuration structs.

Topcoat provides a powerful context system for managing state in asynchronous web applications. Learning how to use app context for sharing values across requests in Topcoat allows you to efficiently share resources like connection pools and caches without cloning data per request. This guide covers the implementation details, registration methods, and retrieval patterns based on the current tokio-rs/topcoat source code.

Understanding App Context vs Request Context

Topcoat distinguishes between two scopes of state storage within the request context (Cx):

  • App context: Global to the router and shared by every request. It lives for the entire lifetime of the Router and is not cloned per request. Handlers receive a reference (&T), making it ideal for database pools, configuration structs, or caches.

  • Request context: Specific to a single request and dropped when the request ends. This is used for request-specific data like parsed form inputs or authentication results.

Both contexts use a type-keyed storage system backed by ContextMap in crates/topcoat-core/src/context/context_map.rs. This guarantees that each concrete type (T) can only be registered once, preventing accidental overwrites and providing compile-time safety through generics bounded by Any + Send + Sync.

Registering App Context Values

You register app context values while building the router using the builder pattern. The Router::app_context method (defined in crates/topcoat-router/src/router.rs) accepts any type that implements Send + Sync and stores it in the global context map.

use topcoat::{context::{Cx, app_context}, Router};
use std::sync::atomic::{AtomicU64, Ordering};

#[derive(Debug)]
struct PageViews(AtomicU64);

fn main() {
    let router = Router::builder()
        .discover()                     // auto-register routes
        .app_context(PageViews(AtomicU64::new(0)))
        .app_context(DbPool::new())     // register a DB pool for all requests
        .build();

    topcoat::start(router);
}

The value is stored directly in the router and shared across all requests as a reference, not a clone. This is critical for performance when sharing large structures like database connection pools across thousands of concurrent connections.

Retrieving Shared State in Handlers

Handlers retrieve app context values using the app_context function, which performs a type-keyed lookup in the ContextMap. The function panics if the requested type was not registered, helping catch configuration errors early during development.

use topcoat::context::{Cx, app_context};

async fn home(cx: &Cx) -> impl topcoat::response::Response {
    // Increment the shared page-view counter
    let views = app_context::<PageViews>(cx);
    let count = views.0.fetch_add(1, Ordering::Relaxed) + 1;

    format!("This page has been viewed {count} times")
}

For optional configuration values where the type might not always be registered, use try_app_context, which returns Option<&T> instead of panicking:

use topcoat::context::{Cx, try_app_context};

async fn maybe_feature(cx: &Cx) -> impl topcoat::response::Response {
    if let Some(cfg) = try_app_context::<FeatureConfig>(cx) {
        format!("Feature enabled: {}", cfg.description)
    } else {
        "Feature not configured".into()
    }
}

The implementation of these retrieval functions lives in crates/topcoat-core/src/context/context_map.rs (lines 35-41 for try_app_context and lines 68-79 for app_context).

Cross-Crate Usage Patterns

App context enables middleware and helper crates to access shared resources registered by the main application. For example, the cookie crate accesses a signing key stored in app context:

use topcoat_core::context::{Cx, app_context};
use topcoat_cookie::Key;

fn sign_cookie(cx: &Cx, data: &str) -> String {
    // `Key` was registered via `router.app_context(Key::generate())`
    let key = app_context::<Key>(cx);
    cookies(cx).signed(key).sign(data)
}

This pattern appears in crates/topcoat-cookie/src/lib.rs (lines 283-287), demonstrating how app context facilitates loose coupling between components while maintaining type safety.

Summary

  • Type-keyed storage: Each type can be registered exactly once in the app context, enforced by the ContextMap implementation in crates/topcoat-core/src/context/context_map.rs.
  • Global lifetime: App context values live for the entire Router lifetime and are shared across all requests as references (&T), avoiding per-request cloning overhead.
  • Registration: Use Router::builder().app_context(value) during application startup to inject shared resources like database pools or configuration structs.
  • Retrieval: Use app_context::<T>(cx) for required values (panics if missing) or try_app_context::<T>(cx) for optional values (returns Option<&T>).
  • Thread safety: All app context values must implement Send + Sync, ensuring safe concurrent access across Tokio's async runtime.

Frequently Asked Questions

What happens if I call app_context for a type that wasn't registered?

The application will panic at runtime. According to the implementation in crates/topcoat-core/src/context/context_map.rs, the app_context function expects the type to exist and panics if the lookup fails. This design choice helps catch configuration errors immediately during development rather than silently ignoring missing dependencies. Use try_app_context if you need to handle potentially missing values gracefully.

Is app context thread-safe for concurrent access?

Yes. The ContextMap requires all stored types to implement Send + Sync, and the storage itself uses thread-safe internal mechanisms. Since handlers receive immutable references (&T) to the shared data, you should use interior mutability (like AtomicU64 or RwLock) if you need to modify shared state, as shown in the PageViews example from examples/app-context/src/main.rs.

Can I register multiple instances of the same type in app context?

No. The type-keyed storage system prevents this by design. Since the ContextMap uses the type itself as the lookup key, attempting to register the same type twice would overwrite the previous value. If you need multiple instances of similar data, wrap them in distinct newtype structs or use a collection type like HashMap<String, T> as your context value.

How is app context different from request context?

App context is global to the router and shared across all requests, while request context is specific to individual requests. You access app context using app_context(cx) for data like database pools, and request context using request_context(cx) for data like parsed request bodies or authentication tokens that only apply to the current request.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →