Genairus logoGenAI-R-Us
Genairus logoGenAI-R-Us

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:

  1. Plugin Architecture - Connect to any backend through standardized plugin points
  2. Capacitor Integration - Type-safe data access with generated SDKs
  3. 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

  1. Separation of Concerns - Keep backend logic separate from UI logic
  2. Type Safety - Leverage Capacitor and Smithy for type-safe contracts
  3. Error Handling - Always handle loading, error, and success states
  4. Optimistic Updates - Update UI immediately, sync with backend asynchronously
  5. Caching - Cache frequently accessed data to reduce network requests
  6. Retry Logic - Implement exponential backoff for failed requests
  7. Loading States - Provide feedback during async operations
  8. Authentication - Use secure, token-based authentication

Further Reading