Architecture Patterns

Deep dive into DAPI and BFF architectural patterns

Two Powerful Patterns

Noundry.ts supports two distinct API architecture patterns, each optimized for different use cases and project requirements.

D

DAPI

Direct API

Direct backend that owns all business logic and data. Perfect for new applications where you control the entire stack.

Frontend (HonoX + JSX)
API Routes (Hono.js)
Database / Cache / Queues
Simple architecture
Fast development
Direct database access
Single deployment unit
B

BFF

Backend for Frontend

Intermediary API that orchestrates calls to another backend. Perfect for integrating with existing systems or when you need separation of concerns.

Frontend (HonoX + JSX)
BFF Layer (Services)
Backend API (Any Stack)
Database / Business Logic
Separation of concerns
Works with any backend
Data aggregation & transformation
Frontend-optimized responses

DAPI: Direct API Pattern

Own your entire stack with direct database access

When to Use DAPI

Perfect For:

  • • Building a new application from scratch
  • • You own both frontend and backend
  • • Team uses TypeScript for everything
  • • Need rapid development
  • • Building MVPs or prototypes
  • • Small-to-medium applications
  • • Want simple architecture

Example Projects:

  • • Todo applications
  • • Blog platforms
  • • E-commerce sites (new builds)
  • • Internal tools
  • • CRM systems
  • • Small business websites

DAPI Implementation

In DAPI, routes directly handle all business logic, database access, and service integrations:

packages/server/src/routes/users.ts
import { Hono } from 'hono';
import { db } from '../config/database';
import { users } from '../schema/schema';
import { cache } from '../config/cache';
import { emailQueue } from '../config/queue';
import bcrypt from 'bcryptjs';

const app = new Hono();

app.post('/register', async (c) => {
  const { email, password, name } = await c.req.json();

  // Direct database access via Drizzle ORM
  const hashedPassword = await bcrypt.hash(password, 10);
  const [user] = await db.insert(users).values({
    email,
    password: hashedPassword,
    name
  }).returning();

  // Direct cache access
  await cache.set(`user:${user.id}`, user, 3600);

  // Direct queue access for background jobs
  await emailQueue.add('send-welcome-email', {
    userId: user.id,
    email: user.email
  });

  return c.json({ user }, 201);
});

export default app;

Key Point: All business logic, data access, caching, and queue operations happen directly in the route handler. This is simple, fast, and perfect for applications where you control the entire stack.

DAPI Project Structure

packages/server/
├── src/
│   ├── config/
│   │   ├── database.ts        # Direct Drizzle ORM connection
│   │   ├── cache.ts           # Direct Redis client
│   │   ├── queue.ts           # Direct BullMQ setup
│   │   └── email.ts           # Direct email service
│   ├── middleware/
│   │   └── auth.ts            # JWT generation & validation
│   ├── routes/
│   │   ├── users.ts           # CRUD with direct DB access
│   │   ├── posts.ts           # CRUD with direct DB access
│   │   └── billing.ts         # CRUD with direct DB access
│   └── schema/
│       └── schema.ts          # Drizzle ORM table schemas

BFF: Backend for Frontend Pattern

Frontend-optimized API layer with separation of concerns

When to Use BFF (Backend for Frontend)

Perfect For:

  • • Existing backend API (.NET, Java, Python)
  • • Separate frontend/backend teams
  • • Backend serves multiple clients
  • • Need frontend-specific data transformations
  • • Want separation of concerns
  • • Enterprise or microservices architecture
  • • Building BFF (Backend for Frontend) layer

Example Projects:

  • • Enterprise portal with legacy .NET backend
  • • E-commerce frontend with Java/Spring backend
  • • Dashboard aggregating multiple microservices
  • • Mobile app BFF
  • • Partner portal with third-party APIs
  • • Multi-tenant SaaS platforms

BFF Benefits: Separation of Concerns

Frontend Layer (BFF)

  • • Data aggregation from multiple sources
  • • UI-specific data transformations
  • • Frontend-optimized response formats
  • • Client-specific caching strategies
  • • Error translation for user messages
  • • Authentication token forwarding

Backend Layer (Real API)

  • • Core business logic
  • • Database operations
  • • Domain rules and validation
  • • Authentication & authorization
  • • Serves multiple clients (web, mobile, etc.)
  • • Technology-agnostic (any language)

Key Advantages of Separation

  • Team Independence: Frontend team can work without backend changes
  • Reusability: Backend serves web, mobile, and other clients
  • Flexibility: Change frontend without touching backend logic
  • Scalability: Scale frontend and backend independently

BFF (Backend for Frontend) Service Layer Implementation

Services wrap HTTP calls to the real backend and transform data for frontend consumption:

packages/server/src/services/UserService.ts
export class UserService {
  private backendUrl: string;

  constructor(backendUrl: string) {
    this.backendUrl = backendUrl;
  }

  async register(data: RegisterDto, bearerToken?: string) {
    // Call real backend API with bearer token forwarding
    const response = await fetch(`${this.backendUrl}/api/users/register`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        ...(bearerToken && { 'Authorization': `Bearer ${bearerToken}` })
      },
      body: JSON.stringify(data)
    });

    if (!response.ok) {
      throw new Error('Backend registration failed');
    }

    const user = await response.json();

    // Transform backend data for frontend consumption
    return {
      id: user.id,
      email: user.email,
      displayName: user.firstName + ' ' + user.lastName, // Computed
      initials: (user.firstName[0] + user.lastName[0]).toUpperCase(), // UI-specific
      avatarColor: this.generateAvatarColor(user.id) // UI-specific
    };
  }

  private generateAvatarColor(id: number): string {
    const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#FFA07A'];
    return colors[id % colors.length];
  }
}

Key Point: The service layer handles HTTP communication with the backend, forwards bearer tokens for authentication, and transforms raw backend data into UI-friendly formats with computed fields.

BFF Routes: Thin Orchestration Layer

Routes become thin - they just orchestrate service calls:

packages/server/src/routes/users.ts
import { Hono } from 'hono';
import { UserService } from '../services/UserService';
import { env } from '../config/env';

const app = new Hono();
const userService = new UserService(env.BACKEND_API_URL);

app.post('/register', async (c) => {
  // Extract data and bearer token from request
  const data = await c.req.json();
  const bearerToken = c.req.header('Authorization')?.replace('Bearer ', '');

  // Just call the service - all logic is there
  const user = await userService.register(data, bearerToken);

  return c.json({ user }, 201);
});

export default app;

Simplicity: Routes are incredibly simple - extract request data, call the service, return response. All complexity is encapsulated in the service layer.

BFF Capabilities

1. Data Aggregation

Combine multiple backend calls into a single frontend-optimized response:

async getUserDashboard(userId: number, token: string) {
  // Fetch from multiple backend endpoints in parallel
  const [user, orders, billing] = await Promise.all([
    this.userService.getUser(userId, token),
    this.orderService.getUserOrders(userId, token),
    this.billingService.getBillingInfo(userId, token)
  ]);

  // Return aggregated data optimized for frontend
  return {
    user,
    recentOrders: orders.slice(0, 5),
    totalSpent: orders.reduce((sum, o) => sum + o.amount, 0),
    paymentMethod: billing.defaultCard
  };
}

2. Data Transformation

Transform verbose backend data into UI-friendly formats:

async getProducts(token: string) {
  const backendProducts = await this.fetchFromBackend('/products', token);

  // Transform to UI-friendly format with computed fields
  return backendProducts.map(p => ({
    id: p.productId,
    name: p.productName,
    price: `$${(p.priceInCents / 100).toFixed(2)}`,
    inStock: p.inventoryCount > 0,
    stockBadge: p.inventoryCount > 10 ? 'In Stock' :
                p.inventoryCount > 0 ? 'Low Stock' : 'Out of Stock',
    imageUrl: p.images[0]?.url || '/placeholder.png'
  }));
}

3. Bearer Token Forwarding

Automatically forward authentication tokens to backend:

private async fetchFromBackend(endpoint: string, token: string) {
  return fetch(`${this.backendUrl}${endpoint}`, {
    headers: {
      'Authorization': `Bearer ${token}`, // Token passed through
      'Content-Type': 'application/json'
    }
  });
}

4. Response Caching

Cache expensive backend calls for improved performance:

async getProductCatalog(token: string) {
  const cacheKey = 'product:catalog';
  const cached = await getCache(cacheKey);
  if (cached) return cached;

  // Cache miss - fetch from backend
  const products = await this.fetchFromBackend('/products', token);
  await setCache(cacheKey, products, 300); // 5 min TTL

  return products;
}

BFF Project Structure

packages/server/
├── src/
│   ├── config/
│   │   ├── env.ts             # BACKEND_API_URL configuration
│   │   └── cache.ts           # Optional: cache backend responses
│   ├── middleware/
│   │   └── auth.ts            # Pass-through or lightweight validation
│   ├── services/              # ⭐ Services wrap backend API calls
│   │   ├── BaseService.ts     # HTTP client wrapper
│   │   ├── UserService.ts     # User operations + transformations
│   │   ├── BillingService.ts  # Billing operations + transformations
│   │   ├── ProductService.ts  # Product operations + transformations
│   │   └── OrderService.ts    # Order operations + transformations
│   ├── routes/
│   │   ├── users.ts           # Thin - calls UserService
│   │   ├── billing.ts         # Thin - calls BillingService
│   │   └── products.ts        # Thin - calls ProductService
│   └── types/
│       └── backend-dtos.ts    # DTOs for backend communication

Side-by-Side Comparison

Choose the right pattern for your project

Feature DAPI BFF
Complexity Low (single API) Medium (API + Services)
Development Speed Fast Moderate
Best For New apps, MVPs, small-medium Existing backend, enterprise
Database Access Direct via Drizzle ORM Via backend API (HTTP)
Business Logic In routes/controllers In backend API
Data Transformation Minimal Extensive (UI-optimized)
Auth Pattern JWT generation in API Token pass-through to backend
Team Structure Full-stack or single team Split frontend/backend teams
Technology Lock-in TypeScript/Bun/Node Backend agnostic (any language)
Performance Fast (direct DB) Slower (extra network hop)
Scalability Vertical Horizontal (independent scaling)
Deployment Monolithic or serverless Separate frontend/backend

Decision Tree

Use this checklist to choose the right pattern

Choose DAPI if:

  • Building a new application from scratch
  • You own both frontend and backend
  • Team uses TypeScript for everything
  • Want fast development with minimal complexity
  • Don't need to integrate with existing systems
  • Building MVP, prototype, or small-medium app

Examples: Todo apps, blogs, e-commerce sites, internal tools, CRM systems

Choose BFF (Backend for Frontend) if:

  • Have an existing backend API
  • Backend is in a different language (.NET, Java, Python)
  • Backend team is separate from frontend team
  • Need to aggregate data from multiple services
  • Backend serves multiple clients (web, mobile)
  • Want frontend-specific data transformations
  • Building enterprise or large-scale applications

Examples: Enterprise portals, multi-client systems, dashboards, mobile BFF, partner portals

Environment Configuration

Configure your environment based on the chosen pattern

D DAPI Environment

.env
# Database
DB_URL=postgresql://user:pass@host/db

# Auth
JWT_SECRET=your-secret-key

# Optional Services
REDIS_URL=redis://localhost:6379
SMTP_HOST=smtp.example.com
S3_BUCKET=my-uploads

B BFF Environment

.env
# Backend API Configuration
BACKEND_API_URL=https://api.yourbackend.com
BACKEND_API_TIMEOUT=30000

# Optional: Auth validation
JWT_SECRET=your-secret-key

# Optional: Cache backend responses
REDIS_URL=redis://localhost:6379
CACHE_TTL=300

Ready to Choose Your Architecture?

Run the CLI and select your preferred pattern during project generation

bunx @noundryfx/ndts-cli init