Lesson 09-Bun.js Project Architecture and Engineering

Project Architecture Design

SPA and MPA Architecture Design

Single Page Application (SPA) Architecture:

// src/router.ts (Bun-based SPA routing implementation)
import { serve } from 'bun'

const app = serve({
  port: 3000,
  fetch(request) {
    const url = new URL(request.url)
    const pathname = url.pathname

    // Dynamically import page components (code splitting)
    const getPage = async () => {
      switch (pathname) {
        case '/':
          return (await import('./pages/Home')).default
        case '/about':
          return (await import('./pages/About')).default
        default:
          return (await import('./pages/404')).default
      }
    }

    try {
      const Page = await getPage()
      const html = await Page.render()
      return new Response(html, {
        headers: { 'Content-Type': 'text/html' }
      })
    } catch (err) {
      console.error('Routing error:', err)
      return new Response('Not Found', { status: 404 })
    }
  }
})

console.log('SPA running on http://localhost:3000')

Multi-Page Application (MPA) Architecture:

// src/server.ts (Bun-based MPA server)
import { serve } from 'bun'
import fs from 'fs/promises'
import path from 'path'

const app = serve({
  port: 3000,
  fetch(request) {
    const url = new URL(request.url)
    const pathname = url.pathname

    // MPA route mapping
    const pageMap = {
      '/': '/pages/home.html',
      '/about': '/pages/about.html',
      '/contact': '/pages/contact.html'
    }

    if (pageMap[pathname]) {
      return serveStaticFile(pageMap[pathname])
    }
    return new Response('Not Found', { status: 404 })
  }
})

async function serveStaticFile(filePath: string) {
  const fullPath = path.join(process.cwd(), filePath)
  try {
    const content = await fs.readFile(fullPath, 'utf8')
    return new Response(content, {
      headers: { 'Content-Type': getContentType(filePath) }
    })
  } catch {
    return new Response('Not Found', { status: 404 })
  }
}

function getContentType(filePath: string) {
  const ext = path.extname(filePath)
  const mimeTypes = {
    '.html': 'text/html',
    '.css': 'text/css',
    '.js': 'application/javascript'
  }
  return mimeTypes[ext] || 'application/octet-stream'
}

console.log('MPA running on http://localhost:3000')

Modular and Component-Based Design

Modular Architecture Example:

src/
├── modules/
   ├── auth/          # Authentication module
      ├── api.ts     # API requests
      ├── hooks.ts   # Custom hooks
      └── types.ts   # Type definitions
   ├── user/          # User module
   └── product/       # Product module
├── shared/            # Shared module
   ├── components/    # Common components
   ├── utils/         # Utility functions
   └── constants.ts   # Constant definitions

Component-Based Design Principles:

// src/shared/components/Button.tsx
import type { ButtonProps } from './types'

export const Button = ({ 
  variant = 'primary',
  size = 'medium',
  ...props 
}: ButtonProps) => {
  const baseClasses = 'rounded font-medium transition-colors'
  const variantClasses = {
    primary: 'bg-blue-500 text-white hover:bg-blue-600',
    secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300'
  }
  const sizeClasses = {
    small: 'px-3 py-1 text-sm',
    medium: 'px-4 py-2 text-base',
    large: 'px-6 py-3 text-lg'
  }

  return (
    <button
      className={`${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]}`}
      {...props}
    />
  )
}

// Type definitions (src/shared/components/types.ts)
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary'
  size?: 'small' | 'medium' | 'large'
}

State Management Architecture

Zustand State Management Example:

// src/stores/counterStore.ts
import { create } from 'zustand'

interface CounterState {
  count: number
  increment: () => void
  decrement: () => void
  reset: () => void
}

export const useCounterStore = create<CounterState>((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
  reset: () => set({ count: 0 })
}))

// Usage in component
import { useCounterStore } from '../stores/counterStore'

export const Counter = () => {
  const { count, increment, decrement } = useCounterStore()
  return (
    <div>
      <button onClick={decrement}>-</button>
      <span>{count}</span>
      <button onClick={increment}>+</button>
    </div>
  )
}

Redux Toolkit Integration:

// src/stores/store.ts
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './counterSlice'

export const store = configureStore({
  reducer: {
    counter: counterReducer
  }
})

// src/stores/counterSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit'

interface CounterState {
  value: number
}

const initialState: CounterState = {
  value: 0
}

export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment: (state) => {
      state.value += 1
    },
    decrement: (state) => {
      state.value -= 1
    }
  }
})

export const { increment, decrement } = counterSlice.actions
export default counterSlice.reducer

Routing Architecture Design

Dynamic Routing Implementation:

// src/router/dynamicRoutes.ts
type RouteConfig = {
  path: string
  component: () => Promise<{ default: React.ComponentType }>
  children?: RouteConfig[]
}

export const routes: RouteConfig[] = [
  {
    path: '/',
    component: () => import('../pages/Home')
  },
  {
    path: '/products/:id',
    component: () => import('../pages/ProductDetail'),
    children: [
      {
        path: 'reviews',
        component: () => import('../components/ProductReviews')
      }
    ]
  }
]

// Route matcher
export function matchRoute(url: string): RouteConfig | null {
  const pathSegments = url.split('/').filter(Boolean)

  for (const route of routes) {
    const routeSegments = route.path.split('/').filter(Boolean)

    if (pathSegments.length !== routeSegments.length) continue

    let match = true
    const params: Record<string, string> = {}

    for (let i = 0; i < routeSegments.length; i++) {
      if (routeSegments[i].startsWith(':')) {
        params[routeSegments[i].slice(1)] = pathSegments[i]
      } else if (routeSegments[i] !== pathSegments[i]) {
        match = false
        break
      }
    }

    if (match) {
      return { ...route, params }
    }
  }

  return null
}

Project Directory Conventions

Recommended Directory Structure:

my-bun-app/
├── public/               # Static assets
   ├── favicon.ico
   └── assets/
├── src/
   ├── app.ts            # Application entry point
   ├── components/       # Common components
   ├── layouts/          # Layout components
   ├── pages/            # Page components
   ├── routes/           # Route configurations
   ├── stores/           # State management
   ├── styles/           # Global styles
   ├── types/            # Type definitions
   ├── utils/            # Utility functions
   └── api/              # API request encapsulation
├── tests/                # Test files
├── bunfig.toml           # Bun configuration
├── package.json
└── tsconfig.json

File Naming Conventions:

  • Component files: PascalCase.tsx (e.g., Button.tsx)
  • Utility functions: camelCase.ts (e.g., formatDate.ts)
  • Type definitions: PascalCase.d.ts (e.g., User.d.ts)
  • Configuration files: kebab-case (e.g., bunfig.toml)

Engineering Toolchain

Advanced Bun.js Configuration

bunfig.toml Configuration Example:

# Cache configuration
cache_dir = "~/.bun_cache"

# Installation configuration
install_global_bin_dir = "~/.bun_bin"

# Build configuration
[build]
target = "browser"  # or "node"
minify = true
sourcemap = true
splitting = true    # Enable code splitting

# Development server configuration
[serve]
port = 3000
cors = true
https = false

# Environment variables
[env]
NODE_ENV = "development"
API_BASE_URL = "http://localhost:8080"

Environment Variable Management:

// src/config.ts
type Env = 'development' | 'production' | 'test'

const getEnv = (): Env => {
  return (process.env.NODE_ENV as Env) || 'development'
}

export const config = {
  env: getEnv(),
  apiBaseUrl: process.env.API_BASE_URL || 'http://localhost:8080',
  isDev: getEnv() === 'development'
}

// Usage example
import { config } from './config'
console.log(config.apiBaseUrl)

Webpack Deep Configuration

Bun vs. Webpack Configuration Comparison:

// webpack.config.js (traditional configuration)
module.exports = {
  entry: './src/index.tsx',
  output: {
    filename: '[name].[contenthash].js',
    path: path.resolve(__dirname, 'dist')
  },
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'ts-loader',
        exclude: /node_modules/
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader', 'postcss-loader']
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './public/index.html'
    }),
    new CleanWebpackPlugin()
  ],
  optimization: {
    splitChunks: {
      chunks: 'all'
    }
  }
}

// Equivalent Bun configuration (via bunfig.toml)
[build]
target = "browser"
minify = true
sourcemap = true
splitting = true

[install]
external = ["react", "react-dom"]  # Similar to Webpack's externals

TypeScript Integration

tsconfig.json Configuration:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "lib": ["DOM", "DOM.Iterable", "ES2020"],
    "moduleResolution": "Node",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    },
    "types": ["bun-types"]
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

Type Safety Practices:

// src/types/api.ts
export interface User {
  id: number
  name: string
  email: string
}

export type ApiResponse<T> = {
  data: T
  error?: {
    code: number
    message: string
  }
}

// src/api/userApi.ts
import type { User, ApiResponse } from '../types/api'

export const fetchUser = async (id: number): Promise<ApiResponse<User>> => {
  const response = await fetch(`/api/users/${id}`)
  if (!response.ok) {
    throw new Error('Failed to fetch user')
  }
  return response.json()
}

Code Linting Tools

ESLint Configuration:

// .eslintrc.js
module.exports = {
  env: {
    browser: true,
    es2020: true,
    node: true
  },
  extends: [
    'eslint:recommended',
    'plugin:react/recommended',
    'plugin:@typescript-eslint/recommended'
  ],
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaFeatures: {
      jsx: true
    },
    ecmaVersion: 2020,
    sourceType: 'module'
  },
  plugins: ['react', '@typescript-eslint'],
  rules: {
    'react/react-in-jsx-scope': 'off',
    '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }]
  }
}

Prettier Configuration:

// .prettierrc
{
  "printWidth": 100,
  "tabWidth": 2,
  "useTabs": false,
  "semi": true,
  "singleQuote": true,
  "quoteProps": "as-needed",
  "jsxSingleQuote": false,
  "trailingComma": "all",
  "bracketSpacing": true,
  "bracketSameLine": false,
  "arrowParens": "always"
}

Testing Framework Integration

Vitest Configuration:

// vitest.config.ts
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'jsdom',
    globals: true,
    setupFiles: ['./src/test/setup.ts'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html']
    }
  }
})

Test Example:

// src/components/Button.test.tsx
import { describe, it, expect } from 'vitest'
import { render, screen } from '@testing-library/react'
import { Button } from './Button'

describe('Button component', () => {
  it('renders correctly', () => {
    render(<Button>Click me</Button>)
    expect(screen.getByText('Click me')).toBeInTheDocument()
  })

  it('calls onClick handler', () => {
    const handleClick = vi.fn()
    render(<Button onClick={handleClick}>Click me</Button>)
    screen.getByText('Click me').click()
    expect(handleClick).toHaveBeenCalledTimes(1)
  })
})

Performance Optimization and Deployment

Code Splitting and Lazy Loading

Dynamic Import Implementation:

// src/utils/lazyLoad.ts
export function lazyLoad<T extends React.ComponentType<any>>(
  importFn: () => Promise<{ default: T }>
) {
  return React.lazy(importFn)
}

// Usage example
const LazyComponent = lazyLoad(() => import('./HeavyComponent'))

export const MyComponent = () => (
  <React.Suspense fallback={<div>Loading...</div>}>
    <LazyComponent />
  </React.Suspense>
)

Route-Level Code Splitting:

// src/routes/dynamicRoutes.ts
export const routes = [
  {
    path: '/dashboard',
    component: () => import('../pages/Dashboard'), // Automatic code splitting
    children: [
      {
        path: 'analytics',
        component: () => import('../pages/Analytics') // Nested route code splitting
      }
    ]
  }
]

Asset Compression and Caching

Bun Static Asset Handling:

// src/server/staticAssets.ts
import { serve } from 'bun'

const assetServer = serve({
  port: 3001,
  fetch(request) {
    const url = new URL(request.url)
    const filePath = path.join(process.cwd(), 'public', url.pathname)

    return Bun.file(filePath).response
  }
})

// Enable compression middleware in production
if (process.env.NODE_ENV === 'production') {
  app.use(async (ctx, next) => {
    await next()
    if (ctx.response.body && ctx.response.headers.get('content-type')?.includes('text/')) {
      ctx.response.headers.set('content-encoding', 'gzip')
      ctx.response.body = await compressGzip(ctx.response.body)
    }
  })
}

CDN Integration Configuration:

// src/config/cdn.ts
export const CDN_CONFIG = {
  enabled: process.env.NODE_ENV === 'production',
  baseUrl: 'https://cdn.example.com/assets',
  version: process.env.CDN_VERSION || '1.0.0'
}

// Usage example
const imageUrl = `${CDN_CONFIG.baseUrl}/images/logo.png?v=${CDN_CONFIG.version}`

SSR and SSG Implementation

Server-Side Rendering Example:

// src/server/render.ts
import { renderToString } from 'react-dom/server'
import App from '../app'

export async function renderApp(req: Request) {
  const html = renderToString(<App />)
  return `
    <!DOCTYPE html>
    <html>
      <head><title>SSR App</title></head>
      <body>
        <div id="root">${html}</div>
        <script src="/client.js"></script>
      </body>
    </html>
  `
}

// Server entry point
serve({
  port: 3000,
  fetch(request) {
    if (request.url === '/server-render') {
      return renderApp(request)
    }
    // ...other route handling
  }
})

Static Site Generation:

// scripts/generateStatic.ts
import fs from 'fs/promises'
import path from 'path'
import { renderToString } from 'react-dom/server'
import App from '../src/app'

async function generatePage(route: string) {
  const html = renderToString(<App initialRoute={route} />)
  const outputPath = path.join(process.cwd(), 'dist', route, 'index.html')
  await fs.mkdir(path.dirname(outputPath), { recursive: true })
  await fs.writeFile(outputPath, `
    <!DOCTYPE html>
    <html>
      <head><title>${route}</title></head>
      <body>
        <div id="root">${html}</div>
      </body>
    </html>
  `)
}

// Generate predefined routes
['/', '/about', '/contact'].forEach(generatePage)

Performance Monitoring Solutions

Web Vitals Integration:

// src/utils/performance.ts
import { getCLS, getFID, getLCP } from 'web-vitals'

export function setupPerformanceMonitoring() {
  if (process.env.NODE_ENV === 'production') {
    getCLS(console.log)
    getFID(console.log)
    getLCP(console.log)

    // Custom metric monitoring
    const navigationTiming = performance.getEntriesByType('navigation')[0]
    console.log('Page load time:', navigationTiming.domContentLoadedEventEnd)
  }
}

// Call at application entry
setupPerformanceMonitoring()

Lighthouse CI Configuration:

# .github/workflows/lighthouse.yml
name: Lighthouse Audit

on: [push]

jobs:
  lighthouse:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: 18
      - run: npm ci
      - run: npm run build
      - run: npm install -g @lhci/cli
      - run: lhci autorun --collect.url=https://your-production-url.com

CI/CD and Docker

GitHub Actions Configuration:

# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: 18
      - run: bun install
      - run: bun build
      - uses: azure/webapps-deploy@v2
        with:
          app-name: 'my-bun-app'
          publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }}
          package: ./dist

Dockerfile Optimization:

# Multi-stage build
FROM node:18-alpine as builder

WORKDIR /app
COPY . .
RUN bun install --production
RUN bun build

# Production image
FROM node:18-alpine

WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json .
RUN bun install --production

EXPOSE 3000
CMD ["bun", "run", "--bun", "dist/app.js"]

# Security hardening
RUN apk add --no-cache tini
ENTRYPOINT ["/sbin/tini", "--"]
Share your love