Flux Backend Integration
Version 1.0
Overview
Flux components need data. This guide covers how to connect your Flux UIs to backends—whether they're generated by Capacitor and Smithy or built with traditional REST, GraphQL, or gRPC APIs.
Integration Patterns
Flux supports three primary backend integration patterns:
- Plugin Architecture - Connect to any backend through standardized plugin points
- Capacitor Integration - Type-safe data access with generated SDKs
- Smithy Integration - Type-safe API clients with operation contracts
Plugin Architecture
The Backend Plugin Interface
Every Flux component can declare backend dependencies through the backend block:
component UserProfile {
props: {
userId: String
}
state: {
user: User?
loading: Boolean = true
error: String?
}
// Backend plugin point
backend: {
// Define the operations you need
operations: {
fetchUser: (userId: String) => User
updateUser: (userId: String, data: UserUpdate) => User
}
}
effects: {
onMount() {
this.state.loading = true
try {
this.state.user = await this.backend.fetchUser(props.userId)
} catch (e) {
this.state.error = e.message
} finally {
this.state.loading = false
}
}
}
layout: VStack {
children: [
if (state.loading) LoadingSpinner {},
if (state.error) ErrorAlert { message: state.error },
if (state.user) UserCard { user: state.user }
]
}
}
Implementing Backend Plugins
Create a backend plugin that implements the operations:
// UserProfileBackend.ts
import { BackendPlugin } from '@flux/core';
export class UserProfileBackend implements BackendPlugin {
constructor(private apiClient: ApiClient) {}
async fetchUser(userId: string): Promise<User> {
const response = await this.apiClient.get(`/users/${userId}`);
return response.data;
}
async updateUser(userId: string, data: UserUpdate): Promise<User> {
const response = await this.apiClient.patch(`/users/${userId}`, data);
return response.data;
}
}
Wiring Plugins
Connect plugins to your components:
// App.tsx
import { UserProfile } from './generated/UserProfile';
import { UserProfileBackend } from './backends/UserProfileBackend';
import { apiClient } from './api';
function App() {
const backend = new UserProfileBackend(apiClient);
return (
<UserProfile
userId="123"
backend={backend}
/>
);
}
Capacitor Integration
Capacitor generates type-safe data access SDKs. Flux can reference Capacitor schemas directly.
Defining the Capacitor Schema
// schema.capacitor
model User {
id: UUID @primary
createdAt: Timestamp @default(now())
email: String @unique @required
name: String @required
avatar: String?
bio: String?
}
model Post {
id: UUID @primary
createdAt: Timestamp @default(now())
title: String @required
content: String @required
authorId: UUID @required
author: User @relation(fields: [authorId], references: [id])
published: Boolean @default(false)
}
Configuring Flux with Capacitor
# flux.config.yaml
version: "1.0"
backend:
capacitor:
schema: "./data/schema.capacitor"
generator: "typescript"
Using Capacitor in Flux Components
@backend(
type: "capacitor",
schema: "./schema.capacitor"
)
component PostList {
props: {
authorId: String
}
state: {
posts: List<Post> = []
loading: Boolean = true
}
// Capacitor auto-generates these queries
backend: {
queries: {
authorPosts: capacitor.post.findMany({
where: { authorId: props.authorId },
include: { author: true },
orderBy: { createdAt: "desc" }
})
}
}
effects: {
onMount() {
this.state.loading = true
this.state.posts = await this.backend.queries.authorPosts
this.state.loading = false
}
}
layout: VStack {
children: state.posts.map(post => PostCard { post: post })
}
}
Generated Capacitor Client
Flux generators automatically create the necessary Capacitor client code:
// Generated by Flux + Capacitor
import { capacitor } from './generated/capacitor';
export function PostList({ authorId }: PostListProps) {
const [posts, setPosts] = useState<Post[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchPosts() {
setLoading(true);
const result = await capacitor.post.findMany({
where: { authorId },
include: { author: true },
orderBy: { createdAt: "desc" }
});
setPosts(result);
setLoading(false);
}
fetchPosts();
}, [authorId]);
// ... rest of component
}
Smithy Integration
Smithy generates type-safe API clients. Flux can reference Smithy operations directly.
Defining the Smithy Service
// service.smithy
namespace com.example
service UserService {
version: "1.0"
operations: [GetUser, UpdateUser, ListUsers]
}
@readonly
@http(method: "GET", uri: "/users/{userId}")
operation GetUser {
input: GetUserInput
output: GetUserOutput
errors: [UserNotFound]
}
structure GetUserInput {
@required
@httpLabel
userId: String
}
structure GetUserOutput {
@required
user: User
}
structure User {
@required
id: String
@required
email: String
@required
name: String
avatar: String
bio: String
}
Configuring Flux with Smithy
# flux.config.yaml
version: "1.0"
backend:
smithy:
spec: "./api/service.smithy"
client: "typescript"
Using Smithy in Flux Components
@backend(
type: "smithy",
service: "UserService",
spec: "./service.smithy"
)
component UserProfile {
props: {
userId: String
}
state: {
user: User?
loading: Boolean = true
}
// Smithy operations are available
backend: {
operations: smithy.UserService
}
effects: {
onMount() {
this.state.loading = true
const response = await this.backend.operations.GetUser({
userId: props.userId
})
this.state.user = response.user
this.state.loading = false
}
}
layout: VStack {
children: [
if (state.user) UserCard { user: state.user }
]
}
}
Generated Smithy Client
// Generated by Flux + Smithy
import { UserServiceClient } from './generated/smithy';
export function UserProfile({ userId }: UserProfileProps) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const client = new UserServiceClient({
region: 'us-east-1'
});
useEffect(() => {
async function fetchUser() {
setLoading(true);
const response = await client.GetUser({ userId });
setUser(response.user);
setLoading(false);
}
fetchUser();
}, [userId]);
// ... rest of component
}
REST API Integration
For traditional REST APIs without Capacitor or Smithy:
Defining REST Operations
component ProductList {
state: {
products: List<Product> = []
}
backend: {
baseUrl: "https://api.example.com"
operations: {
fetchProducts: {
method: "GET"
path: "/products"
response: List<Product>
}
createProduct: {
method: "POST"
path: "/products"
body: ProductInput
response: Product
}
}
}
effects: {
onMount() {
this.state.products = await this.backend.fetchProducts()
}
}
}
Generated REST Client
// Generated REST client
class ProductListBackend {
private baseUrl = "https://api.example.com";
async fetchProducts(): Promise<Product[]> {
const response = await fetch(`${this.baseUrl}/products`);
return response.json();
}
async createProduct(input: ProductInput): Promise<Product> {
const response = await fetch(`${this.baseUrl}/products`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(input)
});
return response.json();
}
}
GraphQL Integration
Defining GraphQL Queries
component UserProfile {
props: {
userId: String
}
backend: {
type: "graphql"
endpoint: "https://api.example.com/graphql"
queries: {
user: """
query GetUser($userId: ID!) {
user(id: $userId) {
id
name
email
avatar
posts {
id
title
createdAt
}
}
}
"""
}
mutations: {
updateUser: """
mutation UpdateUser($userId: ID!, $input: UserInput!) {
updateUser(id: $userId, input: $input) {
id
name
email
}
}
"""
}
}
state: {
user: User?
}
effects: {
onMount() {
const result = await this.backend.queries.user({
userId: props.userId
})
this.state.user = result.user
}
}
}
Generated GraphQL Client
// Generated GraphQL client
import { GraphQLClient } from 'graphql-request';
class UserProfileBackend {
private client = new GraphQLClient('https://api.example.com/graphql');
async user(variables: { userId: string }) {
return this.client.request(
`query GetUser($userId: ID!) {
user(id: $userId) {
id
name
email
avatar
posts {
id
title
createdAt
}
}
}`,
variables
);
}
async updateUser(variables: { userId: string; input: UserInput }) {
return this.client.request(
`mutation UpdateUser($userId: ID!, $input: UserInput!) {
updateUser(id: $userId, input: $input) {
id
name
email
}
}`,
variables
);
}
}
State Management
Local State
Simple component-level state:
component Counter {
state: {
count: Integer = 0
}
handlers: {
increment() {
this.state.count += 1
}
decrement() {
this.state.count -= 1
}
}
layout: HStack {
children: [
Button {
label: "-"
onClick: handlers.decrement
},
Text { text: state.count },
Button {
label: "+"
onClick: handlers.increment
}
]
}
}
Derived State
Compute values from state:
component ShoppingCart {
state: {
items: List<CartItem> = []
}
computed: {
subtotal: state.items.reduce((sum, item) =>
sum + (item.price * item.quantity), 0
)
tax: this.subtotal * 0.08
total: this.subtotal + this.tax
isEmpty: state.items.length === 0
}
layout: VStack {
children: [
Text { text: `Subtotal: $${computed.subtotal}` },
Text { text: `Tax: $${computed.tax}` },
Text { text: `Total: $${computed.total}` }
]
}
}
Global State
Share state across components:
// Define global store
store AuthStore {
state: {
user: User?
isAuthenticated: Boolean = false
}
actions: {
login(email: String, password: String) {
const user = await backend.auth.login(email, password)
this.state.user = user
this.state.isAuthenticated = true
}
logout() {
this.state.user = null
this.state.isAuthenticated = false
}
}
}
// Use store in components
component Header {
store: AuthStore
layout: HStack {
children: [
if (store.isAuthenticated) {
UserMenu { user: store.user }
} else {
Button {
label: "Log In"
onClick: navigateTo("/login")
}
}
]
}
}
Real-Time Data
WebSocket Integration
component ChatRoom {
props: {
roomId: String
}
state: {
messages: List<Message> = []
connected: Boolean = false
}
backend: {
websocket: {
url: `wss://api.example.com/chat/${props.roomId}`
events: {
onConnect() {
this.state.connected = true
}
onDisconnect() {
this.state.connected = false
}
onMessage(message: Message) {
this.state.messages.push(message)
}
}
}
operations: {
sendMessage(text: String) {
this.backend.websocket.send({
type: "message",
text: text
})
}
}
}
layout: VStack {
children: [
MessageList { messages: state.messages },
MessageInput { onSend: backend.sendMessage }
]
}
}
Server-Sent Events (SSE)
component LiveFeed {
backend: {
sse: {
url: "https://api.example.com/events"
events: {
onUpdate(data: FeedUpdate) {
this.state.items.unshift(data)
}
}
}
}
state: {
items: List<FeedItem> = []
}
layout: VStack {
children: state.items.map(item => FeedItem { item: item })
}
}
Authentication & Authorization
Auth Context
// Define auth provider
provider AuthProvider {
state: {
user: User?
token: String?
}
backend: {
operations: {
login: (email: String, password: String) => AuthResponse
logout: () => void
refresh: () => AuthResponse
}
}
effects: {
onMount() {
// Restore session from storage
const token = localStorage.getItem('auth_token')
if (token) {
this.state.token = token
const user = await this.backend.refresh()
this.state.user = user
}
}
}
}
// Use auth in components
component ProtectedPage {
context: AuthProvider
layout: VStack {
children: [
if (context.user) {
PageContent { user: context.user }
} else {
LoginPrompt {}
}
]
}
}
Role-Based Access
component AdminPanel {
context: AuthProvider
computed: {
isAdmin: context.user?.role === "admin"
}
layout: VStack {
children: [
if (computed.isAdmin) {
AdminControls {}
} else {
AccessDenied {}
}
]
}
}
Error Handling
Error Boundaries
component ErrorBoundary {
state: {
error: Error?
}
handlers: {
onError(error: Error) {
this.state.error = error
console.error(error)
}
retry() {
this.state.error = null
}
}
layout: VStack {
children: [
if (state.error) {
ErrorDisplay {
error: state.error
onRetry: handlers.retry
}
} else {
props.children
}
]
}
}
Network Error Handling
component DataLoader {
state: {
data: Data?
loading: Boolean = true
error: NetworkError?
}
backend: {
operations: {
fetchData: () => Data
}
}
effects: {
onMount() {
this.state.loading = true
try {
this.state.data = await this.backend.fetchData()
} catch (error) {
if (error.status === 404) {
this.state.error = { type: "not_found" }
} else if (error.status >= 500) {
this.state.error = { type: "server_error" }
} else {
this.state.error = { type: "unknown", message: error.message }
}
} finally {
this.state.loading = false
}
}
}
layout: VStack {
children: [
if (state.loading) LoadingSpinner {},
if (state.error) ErrorDisplay { error: state.error },
if (state.data) DataDisplay { data: state.data }
]
}
}
Best Practices
- Separation of Concerns - Keep backend logic separate from UI logic
- Type Safety - Leverage Capacitor and Smithy for type-safe contracts
- Error Handling - Always handle loading, error, and success states
- Optimistic Updates - Update UI immediately, sync with backend asynchronously
- Caching - Cache frequently accessed data to reduce network requests
- Retry Logic - Implement exponential backoff for failed requests
- Loading States - Provide feedback during async operations
- Authentication - Use secure, token-based authentication