Skip to content

Instantly share code, notes, and snippets.

@kristoferjoseph
Created August 9, 2025 19:26
Show Gist options
  • Save kristoferjoseph/a838c0fd183b2ed3246b140d1d511d8c to your computer and use it in GitHub Desktop.
Save kristoferjoseph/a838c0fd183b2ed3246b140d1d511d8c to your computer and use it in GitHub Desktop.
VANILLA JAVASCRIPT WITH ES MODULES guide that requires ABSOLUTELY ZERO BUILD STEPS!

Claude.md - Modern Web Development with Web Components & Architect Framework

πŸš€ Executive Summary

This guide provides a comprehensive, test-driven approach to building modern web applications using:

  • Frontend: Web Components with Declarative Shadow DOM for true encapsulation and reusability
  • Backend: Architect Framework for serverless AWS infrastructure with minimal configuration
  • Testing: node-tap for TAP-compliant unit testing with excellent TypeScript support
  • Workflow: Agentic test-driven development with small, verifiable iteration steps

πŸ“¦ ES Modules Import Maps

Why Import Maps Are Game-Changing

Import maps eliminate the need for bundlers by allowing you to define module specifiers declaratively in HTML. This enables:

  • Clean imports without messy relative paths (import { BaseComponent } from 'components/base')
  • CDN integration for external dependencies (import { html } from 'lit-html')
  • Architect route modules served from your own static routes
  • Zero build steps while maintaining modern module organization
  • Dynamic module loading for code splitting without bundlers

Setting Up Import Maps with Architect

Enhanced app.arc configuration:

@app
modern-web-app

@static
fingerprint true
spa false

@http
get /
get /api/components
post /api/components
get /api/health
get /modules/*

@tables
components
  componentId *String
  name **String

Serving ES Modules from Architect Routes

src/http/get-modules-000module/index.js

import { readFileSync, existsSync } from 'fs'
import { join, extname } from 'path'

/**
 * Serve ES modules with proper MIME types from static directory
 * Enables import maps to load modules from /modules/* routes
 */
export async function handler(req) {
  try {
    const modulePath = req.pathParameters?.module || ''
    const filePath = join(process.cwd(), 'src', 'static', 'modules', modulePath)
    
    // Security: prevent directory traversal
    if (modulePath.includes('..') || modulePath.includes('~')) {
      return {
        statusCode: 400,
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({ error: 'Invalid module path' })
      }
    }
    
    if (!existsSync(filePath)) {
      return {
        statusCode: 404,
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({ error: 'Module not found' })
      }
    }
    
    const fileContent = readFileSync(filePath, 'utf-8')
    const extension = extname(filePath)
    
    // Set proper MIME type for ES modules
    let contentType = 'application/javascript'
    if (extension === '.json') {
      contentType = 'application/json'
    } else if (extension === '.css') {
      contentType = 'text/css'
    }
    
    return {
      statusCode: 200,
      headers: {
        'content-type': contentType,
        'cache-control': 'public, max-age=31536000', // 1 year cache
        'access-control-allow-origin': '*',
        'access-control-allow-methods': 'GET',
        'cross-origin-resource-policy': 'cross-origin'
      },
      body: fileContent
    }
  } catch (error) {
    console.error('Error serving module:', error)
    return {
      statusCode: 500,
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ error: 'Failed to serve module' })
    }
  }
}

Enhanced Project Structure for Import Maps

src/static/
β”œβ”€β”€ index.html                    # Main app with import map
β”œβ”€β”€ modules/                      # ES modules served via /modules/*
β”‚   β”œβ”€β”€ components/
β”‚   β”‚   β”œβ”€β”€ base/
β”‚   β”‚   β”‚   └── base-component.js
β”‚   β”‚   β”œβ”€β”€ ui/
β”‚   β”‚   β”‚   β”œβ”€β”€ button-component.js
β”‚   β”‚   β”‚   β”œβ”€β”€ counter-component.js
β”‚   β”‚   β”‚   └── modal-component.js
β”‚   β”‚   └── business/
β”‚   β”‚       β”œβ”€β”€ user-profile.js
β”‚   β”‚       └── data-table.js
β”‚   β”œβ”€β”€ utils/
β”‚   β”‚   β”œβ”€β”€ api-client.js
β”‚   β”‚   β”œβ”€β”€ event-bus.js
β”‚   β”‚   └── storage.js
β”‚   └── services/
β”‚       β”œβ”€β”€ auth-service.js
β”‚       └── data-service.js
β”œβ”€β”€ styles/
β”‚   β”œβ”€β”€ main.css
β”‚   └── components.css
└── assets/
    β”œβ”€β”€ images/
    └── icons/

Complete Import Map Implementation

src/static/index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Modern Web App with Import Maps</title>
  
  <!-- Import Map Definition - NO BUILD STEPS! -->
  <script type="importmap">
  {
    "imports": {
      "components/base": "/modules/components/base/base-component.js",
      "components/ui/": "/modules/components/ui/",
      "components/business/": "/modules/components/business/",
      "utils/": "/modules/utils/",
      "services/": "/modules/services/",
      
      // CDN dependencies - served directly to browser!
      "lit-html": "https://cdn.skypack.dev/lit-html@^3.0.0",
      "htm": "https://cdn.skypack.dev/htm@^3.1.0",
      "router": "https://cdn.skypack.dev/@vaadin/router@^1.7.0"
    }
  }
  </script>
  
  <style>
    body {
      font-family: system-ui, -apple-system, sans-serif;
      margin: 0;
      padding: 2rem;
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      min-height: 100vh;
    }
    
    .app-container {
      max-width: 1200px;
      margin: 0 auto;
      background: rgba(255, 255, 255, 0.95);
      border-radius: 1rem;
      padding: 2rem;
      box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
    }
  </style>
</head>
<body>
  <div class="app-container">
    <h1>πŸš€ Modern Web App</h1>
    <p>Pure ES Modules + Import Maps + Web Components + Architect Framework</p>
    
    <!-- Components load via import map routes -->
    <section class="demo-section">
      <h2>Interactive Components</h2>
      
      <custom-button 
        label="Primary Action" 
        variant="primary" 
        size="large">
      </custom-button>
      
      <custom-button 
        label="Secondary Action" 
        variant="secondary" 
        size="medium">
      </custom-button>
      
      <counter-component 
        initial-value="5" 
        min="0" 
        max="100">
      </counter-component>
      
      <user-profile 
        user-id="123"
        show-avatar="true">
      </user-profile>
    </section>
    
    <section class="api-demo">
      <h2>API Integration</h2>
      <data-table 
        endpoint="/api/components"
        auto-refresh="30000">
      </data-table>
    </section>
  </div>

  <!-- Main application script using import maps -->
  <script type="module">
    // Clean imports thanks to import maps! No relative paths!
    import 'components/ui/button-component.js'
    import 'components/ui/counter-component.js'
    import 'components/business/user-profile.js'
    import 'components/business/data-table.js'
    
    import { EventBus } from 'utils/event-bus.js'
    import { ApiClient } from 'utils/api-client.js'
    import { AuthService } from 'services/auth-service.js'
    
    // Optional: CDN dependencies work seamlessly
    import { html, render } from 'lit-html'
    
    /**
     * Application initialization with pure ES modules
     */
    class App {
      constructor() {
        this.eventBus = new EventBus()
        this.apiClient = new ApiClient('/api')
        this.authService = new AuthService()
        
        this.init()
      }
      
      async init() {
        console.log('πŸš€ App initializing with import maps!')
        
        // Set up global event listeners
        this.setupGlobalEvents()
        
        // Initialize services
        await this.authService.init()
        
        // Set up component communication
        this.setupComponentCommunication()
        
        console.log('βœ… App ready!')
      }
      
      setupGlobalEvents() {
        // Global error handling
        window.addEventListener('error', (e) => {
          console.error('Global error:', e.error)
        })
        
        // Global custom event handling
        document.addEventListener('custom-click', (e) => {
          console.log('Button clicked globally:', e.detail)
        })
      }
      
      setupComponentCommunication() {
        // Example: Counter updates trigger API calls
        document.addEventListener('count-changed', async (e) => {
          console.log('Count changed:', e.detail.count)
          
          // Send to backend via clean API client
          try {
            await this.apiClient.post('/components', {
              type: 'counter-update',
              value: e.detail.count,
              timestamp: new Date().toISOString()
            })
          } catch (error) {
            console.error('Failed to sync counter:', error)
          }
        })
      }
    }
    
    // Start the application
    const app = new App()
  </script>
</body>
</html>

Clean Module Examples Using Import Maps

src/static/modules/utils/api-client.js

/**
 * Clean API client using fetch with proper error handling
 * Loaded via import map: import { ApiClient } from 'utils/api-client.js'
 */
export class ApiClient {
  /**
   * @param {string} baseURL - Base URL for API calls
   */
  constructor(baseURL = '/api') {
    this.baseURL = baseURL
  }
  
  /**
   * GET request with automatic JSON parsing
   * @param {string} endpoint - API endpoint
   * @param {Object} [options={}] - Additional fetch options
   * @returns {Promise<any>} Response data
   */
  async get(endpoint, options = {}) {
    const response = await fetch(`${this.baseURL}${endpoint}`, {
      method: 'GET',
      headers: {
        'Content-Type': 'application/json',
        ...options.headers
      },
      ...options
    })
    
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`)
    }
    
    return response.json()
  }
  
  /**
   * POST request with automatic JSON serialization
   * @param {string} endpoint - API endpoint
   * @param {any} data - Data to send
   * @param {Object} [options={}] - Additional fetch options
   * @returns {Promise<any>} Response data
   */
  async post(endpoint, data, options = {}) {
    const response = await fetch(`${this.baseURL}${endpoint}`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        ...options.headers
      },
      body: JSON.stringify(data),
      ...options
    })
    
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`)
    }
    
    return response.json()
  }
}

src/static/modules/utils/event-bus.js

/**
 * Simple event bus for component communication
 * Loaded via import map: import { EventBus } from 'utils/event-bus.js'
 */
export class EventBus extends EventTarget {
  /**
   * Emit a custom event
   * @param {string} type - Event type
   * @param {any} [detail] - Event detail data
   */
  emit(type, detail) {
    this.dispatchEvent(new CustomEvent(type, { detail }))
  }
  
  /**
   * Listen for events
   * @param {string} type - Event type to listen for
   * @param {Function} listener - Event listener function
   */
  on(type, listener) {
    this.addEventListener(type, listener)
  }
  
  /**
   * Remove event listener
   * @param {string} type - Event type
   * @param {Function} listener - Event listener function
   */
  off(type, listener) {
    this.removeEventListener(type, listener)
  }
}

// Export singleton instance
export const eventBus = new EventBus()

Business Logic Component with Import Maps

src/static/modules/components/business/data-table.js

// Clean imports thanks to import maps!
import { BaseComponent } from 'components/base'
import { ApiClient } from 'utils/api-client.js'
import { eventBus } from 'utils/event-bus.js'

/**
 * Data table component that fetches and displays API data
 * @class DataTableComponent
 * @extends BaseComponent
 */
export class DataTableComponent extends BaseComponent {
  static get observedAttributes() {
    return ['endpoint', 'auto-refresh']
  }
  
  constructor() {
    super()
    this.apiClient = new ApiClient()
    this.data = []
    this.loading = false
    this.refreshInterval = null
  }
  
  get endpoint() {
    return this.getAttribute('endpoint') || '/api/data'
  }
  
  get autoRefresh() {
    const value = this.getAttribute('auto-refresh')
    return value ? parseInt(value, 10) : null
  }
  
  async connectedCallback() {
    super.connectedCallback()
    await this.fetchData()
    this.setupAutoRefresh()
  }
  
  disconnectedCallback() {
    this.cleanup()
  }
  
  render() {
    this.shadowRoot.innerHTML = `
      <style>
        :host {
          display: block;
          margin: 1rem 0;
        }
        
        .table-container {
          border: 1px solid #e5e7eb;
          border-radius: 0.5rem;
          overflow: hidden;
        }
        
        table {
          width: 100%;
          border-collapse: collapse;
        }
        
        th, td {
          padding: 0.75rem;
          text-align: left;
          border-bottom: 1px solid #e5e7eb;
        }
        
        th {
          background-color: #f9fafb;
          font-weight: 600;
        }
        
        .loading {
          text-align: center;
          padding: 2rem;
          color: #6b7280;
        }
        
        .error {
          background-color: #fef2f2;
          color: #dc2626;
          padding: 1rem;
          border-radius: 0.5rem;
        }
      </style>
      
      <div class="table-container">
        ${this.loading ? 
          '<div class="loading">Loading...</div>' : 
          this.renderTable()
        }
      </div>
    `
  }
  
  renderTable() {
    if (this.data.length === 0) {
      return '<div class="loading">No data available</div>'
    }
    
    const headers = Object.keys(this.data[0])
    
    return `
      <table>
        <thead>
          <tr>
            ${headers.map(header => `<th>${header}</th>`).join('')}
          </tr>
        </thead>
        <tbody>
          ${this.data.map(row => `
            <tr>
              ${headers.map(header => `<td>${row[header] || ''}</td>`).join('')}
            </tr>
          `).join('')}
        </tbody>
      </table>
    `
  }
  
  async fetchData() {
    try {
      this.loading = true
      this.render()
      
      const response = await this.apiClient.get(this.endpoint)
      this.data = response.components || response.data || []
      
      this.loading = false
      this.render()
      
      // Emit event for other components
      eventBus.emit('data-loaded', { 
        endpoint: this.endpoint, 
        count: this.data.length 
      })
      
    } catch (error) {
      console.error('Failed to fetch data:', error)
      this.loading = false
      this.showError(error.message)
    }
  }
  
  showError(message) {
    this.shadowRoot.innerHTML = `
      <div class="error">
        Error loading data: ${message}
      </div>
    `
  }
  
  setupAutoRefresh() {
    if (this.autoRefresh && this.autoRefresh > 0) {
      this.refreshInterval = setInterval(() => {
        this.fetchData()
      }, this.autoRefresh)
    }
  }
  
  cleanup() {
    if (this.refreshInterval) {
      clearInterval(this.refreshInterval)
      this.refreshInterval = null
    }
  }
  
  attributeChangedCallback() {
    if (this._initialized) {
      this.cleanup()
      this.fetchData()
      this.setupAutoRefresh()
    }
  }
}

// Register component
customElements.define('data-table', DataTableComponent)

Testing Import Maps Components

test/component/data-table.test.js

import t from 'tap'
import { JSDOM } from 'jsdom'

// Setup DOM environment
const dom = new JSDOM(`<!DOCTYPE html>
<html>
<head>
  <script type="importmap">
  {
    "imports": {
      "components/base": "/modules/components/base/base-component.js",
      "utils/api-client.js": "/modules/utils/api-client.js",
      "utils/event-bus.js": "/modules/utils/event-bus.js"
    }
  }
  </script>
</head>
<body></body>
</html>`)

global.window = dom.window
global.document = dom.window.document
global.customElements = dom.window.customElements
global.HTMLElement = dom.window.HTMLElement
global.CustomEvent = dom.window.CustomEvent
global.fetch = () => Promise.resolve({
  ok: true,
  json: () => Promise.resolve({ components: [{ id: 1, name: 'Test' }] })
})

// Import components using the same paths as import maps
import '../src/static/modules/components/business/data-table.js'

t.test('DataTableComponent with Import Maps', async t => {
  t.test('should fetch and render data', async t => {
    const table = document.createElement('data-table')
    table.setAttribute('endpoint', '/api/test')
    document.body.appendChild(table)
    
    await customElements.whenDefined('data-table')
    
    // Wait for async data loading
    await new Promise(resolve => setTimeout(resolve, 100))
    
    const tableElement = table.shadowRoot.querySelector('table')
    t.ok(tableElement, 'should render table element')
    
    const rows = table.shadowRoot.querySelectorAll('tbody tr')
    t.ok(rows.length > 0, 'should render data rows')
  })
})

Best Practices for Import Maps

  1. Organize by Domain: Group modules logically (components/, utils/, services/)

  2. Use Trailing Slashes: Enable directory imports ("utils/": "/modules/utils/")

  3. Version CDN Dependencies: Pin versions for stability (@^3.0.0)

  4. Cache Static Modules: Use long cache headers for /modules/* routes

  5. Fallback Strategy: Consider what happens if import maps aren't supported:

<script type="module">
  if (!HTMLScriptElement.supports?.('importmap')) {
    console.warn('Import maps not supported')
    // Load polyfill or fallback
  }
</script>
  1. Development vs Production: Use different CDN URLs for different environments

Import maps transform modern web development by eliminating build tools while maintaining the clean, organized module structure we love. Combined with Architect's serverless backend, you get the best of both worlds: modern DX with zero complexity! πŸŽ‰


πŸ“‹ Table of Contents

  1. Project Architecture
  2. Development Environment Setup
  3. Testing Strategy
  4. Web Components Implementation
  5. ES Modules Import Maps
  6. Architect Backend Setup
  7. Agentic Workflow Process
  8. Production Deployment
  9. Best Practices

πŸ—οΈ Project Architecture

Frontend Stack

  • HTML5: Semantic markup with modern standards
  • CSS: Custom properties, Grid, Flexbox, Container queries
  • JavaScript: Pure ES2023+ features with ES Modules - NO BUILD STEPS!
  • Web Components: Custom Elements + Declarative Shadow DOM
  • Runtime: Direct execution in browsers and Node.js - zero compilation

Backend Stack

  • Architect Framework: Serverless AWS infrastructure with declarative configuration
  • Runtime: Node.js 20+ on AWS Lambda with native ES modules
  • Database: DynamoDB with Architect's data layer
  • API: HTTP functions with API Gateway v2

Testing Infrastructure

  • Unit Testing: node-tap with pure JavaScript and comprehensive coverage
  • Component Testing: DOM testing with JSDOM
  • Integration Testing: Full stack testing with local Architect sandbox
  • CI/CD: Automated testing pipeline - NO BUILD STEPS!

πŸ› οΈ Development Environment Setup

Prerequisites

# Required versions
node --version  # v20.0.0+
npm --version   # v10.0.0+

Project Initialization

# Create project structure
mkdir modern-web-app && cd modern-web-app

# Initialize package.json with ES modules
npm init -y
npm pkg set type="module"

# Install core dependencies
npm install @architect/architect @architect/functions
npm install --save-dev tap jsdom

# Install development tools  
npm install --save-dev @architect/sandbox concurrently

Project Structure

modern-web-app/
β”œβ”€β”€ .taprc                     # tap configuration
β”œβ”€β”€ app.arc                    # Architect manifest
β”œβ”€β”€ package.json               # ES modules enabled
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ components/           # Web Components
β”‚   β”‚   β”œβ”€β”€ base/            # Base classes
β”‚   β”‚   β”œβ”€β”€ ui/              # UI components
β”‚   β”‚   └── business/        # Business logic components
β”‚   β”œβ”€β”€ http/                # Architect HTTP functions
β”‚   β”‚   β”œβ”€β”€ get-index/
β”‚   β”‚   β”œβ”€β”€ get-api-*/
β”‚   β”‚   └── post-api-*/
β”‚   β”œβ”€β”€ shared/              # Shared utilities
β”‚   └── static/              # Static assets
β”œβ”€β”€ test/
β”‚   β”œβ”€β”€ unit/                # Unit tests
β”‚   β”œβ”€β”€ component/           # Component tests
β”‚   └── integration/         # Integration tests
└── docs/                    # Documentation

Configuration Files

.taprc

include: 
  - "test/**/*.js"
exclude:
  - "test/fixtures/**"
reporter: tap
coverage: true
check-coverage: true
statements: 90
branches: 90
functions: 90
lines: 90

app.arc

@app
modern-web-app

@static
fingerprint true

@http
get /
get /api/components
post /api/components
get /api/health

@tables
components
  componentId *String
  name **String

πŸ§ͺ Testing Strategy

Testing Philosophy

Follow the "Testing Diamond" approach - focus primarily on component tests that provide high confidence with realistic scenarios

  1. Unit Tests (20%): Test pure functions and utilities
  2. Component Tests (70%): Test Web Components in isolation
  3. Integration Tests (10%): Test full user workflows

Test Structure with node-tap

test/unit/math.test.js

import t from 'tap'
import { add, multiply } from '../../src/shared/math.js'

t.test('Math utilities', async t => {
  t.test('add function', async t => {
    t.equal(add(2, 3), 5, 'should add two numbers correctly')
    t.equal(add(-1, 1), 0, 'should handle negative numbers')
    t.equal(add(0, 0), 0, 'should handle zero')
  })
  
  t.test('multiply function', async t => {
    t.equal(multiply(3, 4), 12, 'should multiply two numbers correctly')
    t.equal(multiply(-2, 3), -6, 'should handle negative numbers')
    t.equal(multiply(0, 5), 0, 'should handle zero')
  })
})

Web Component Testing Pattern

test/component/button-component.test.js

import t from 'tap'
import { JSDOM } from 'jsdom'

// Setup DOM environment for pure JavaScript testing
const dom = new JSDOM(`<!DOCTYPE html><html><body></body></html>`)
global.window = dom.window
global.document = dom.window.document
global.customElements = dom.window.customElements
global.HTMLElement = dom.window.HTMLElement
global.CustomEvent = dom.window.CustomEvent

// Import component after DOM setup - pure ES modules!
import '../src/components/ui/button-component.js'

t.test('ButtonComponent', async t => {
  t.beforeEach(async () => {
    document.body.innerHTML = ''
  })

  t.test('should render with default properties', async t => {
    const button = document.createElement('custom-button')
    button.setAttribute('label', 'Click me')
    document.body.appendChild(button)
    
    // Wait for component to upgrade
    await customElements.whenDefined('custom-button')
    
    const shadowRoot = button.shadowRoot
    t.ok(shadowRoot, 'should have shadow root')
    
    const buttonElement = shadowRoot.querySelector('button')
    t.equal(buttonElement.textContent, 'Click me', 'should display correct label')
  })

  t.test('should handle click events', async t => {
    const button = document.createElement('custom-button')
    button.setAttribute('label', 'Test Button')
    document.body.appendChild(button)
    
    await customElements.whenDefined('custom-button')
    
    let clicked = false
    button.addEventListener('custom-click', () => {
      clicked = true
    })
    
    const buttonElement = button.shadowRoot.querySelector('button')
    buttonElement.click()
    
    t.ok(clicked, 'should emit custom-click event')
  })
})

πŸ”§ Web Components Implementation

Base Component Class

src/components/base/base-component.js

/**
 * Base class for all custom components with common functionality
 * @class BaseComponent
 * @extends HTMLElement
 */
export class BaseComponent extends HTMLElement {
  constructor() {
    super()
    this.attachShadow({ mode: 'open' })
    /** @private {boolean} */
    this._initialized = false
  }

  connectedCallback() {
    if (!this._initialized) {
      this.render()
      this.setupEventListeners()
      this._initialized = true
    }
  }

  disconnectedCallback() {
    this.cleanup()
  }

  /**
   * Override this method to define component template
   * @abstract
   * @throws {Error} When not implemented in subclass
   */
  render() {
    throw new Error('render() method must be implemented')
  }

  /**
   * Override this method to setup event listeners
   * @abstract
   */
  setupEventListeners() {
    // Override in subclasses
  }

  /**
   * Override this method to cleanup resources
   * @abstract
   */
  cleanup() {
    // Override in subclasses
  }

  /**
   * Utility method to emit custom events
   * @param {string} eventName - Name of the event to emit
   * @param {Object} [detail={}] - Event detail object
   */
  emit(eventName, detail = {}) {
    this.dispatchEvent(new CustomEvent(eventName, {
      detail,
      bubbles: true,
      composed: true
    }))
  }

  /**
   * Utility method to safely query shadow DOM
   * @param {string} selector - CSS selector
   * @returns {Element|null} First matching element
   */
  $(selector) {
    return this.shadowRoot.querySelector(selector)
  }

  /**
   * Utility method to safely query all shadow DOM elements
   * @param {string} selector - CSS selector
   * @returns {NodeList} All matching elements
   */
  $(selector) {
    return this.shadowRoot.querySelectorAll(selector)
  }
}

Modern Button Component with Declarative Shadow DOM

src/components/ui/button-component.js

import { BaseComponent } from '../base/base-component.js'

/**
 * Custom button component with modern styling and accessibility
 * Pure JavaScript implementation with no build steps required!
 * @class ButtonComponent
 * @extends BaseComponent
 * 
 * @example
 * <custom-button label="Click me" variant="primary" size="large"></custom-button>
 */
export class ButtonComponent extends BaseComponent {
  /**
   * Observed attributes for reactive updates
   * @static
   * @returns {string[]} Array of attribute names to observe
   */
  static get observedAttributes() {
    return ['label', 'variant', 'disabled', 'size']
  }

  constructor() {
    super()
  }

  /**
   * Get the button label
   * @returns {string} Button label text
   */
  get label() {
    return this.getAttribute('label') || 'Button'
  }

  /**
   * Get the button variant
   * @returns {string} Button variant (primary, secondary)
   */
  get variant() {
    return this.getAttribute('variant') || 'primary'
  }

  /**
   * Check if button is disabled
   * @returns {boolean} True if disabled
   */
  get disabled() {
    return this.hasAttribute('disabled')
  }

  /**
   * Get the button size
   * @returns {string} Button size (small, medium, large)
   */
  get size() {
    return this.getAttribute('size') || 'medium'
  }

  render() {
    this.shadowRoot.innerHTML = `
      <style>
        :host {
          display: inline-block;
          --button-primary: #2563eb;
          --button-primary-hover: #1d4ed8;
          --button-secondary: #6b7280;
          --button-secondary-hover: #4b5563;
          --button-border-radius: 0.5rem;
          --button-font-weight: 500;
        }

        button {
          position: relative;
          display: inline-flex;
          align-items: center;
          justify-content: center;
          border: none;
          border-radius: var(--button-border-radius);
          font-weight: var(--button-font-weight);
          font-family: inherit;
          cursor: pointer;
          transition: all 0.2s ease-in-out;
          outline: none;
          text-decoration: none;
        }

        button:focus-visible {
          outline: 2px solid var(--button-primary);
          outline-offset: 2px;
        }

        /* Variants */
        .primary {
          background-color: var(--button-primary);
          color: white;
        }

        .primary:hover:not(:disabled) {
          background-color: var(--button-primary-hover);
          transform: translateY(-1px);
        }

        .secondary {
          background-color: var(--button-secondary);
          color: white;
        }

        .secondary:hover:not(:disabled) {
          background-color: var(--button-secondary-hover);
          transform: translateY(-1px);
        }

        /* Sizes */
        .small {
          padding: 0.5rem 1rem;
          font-size: 0.875rem;
        }

        .medium {
          padding: 0.75rem 1.5rem;
          font-size: 1rem;
        }

        .large {
          padding: 1rem 2rem;
          font-size: 1.125rem;
        }

        /* States */
        button:disabled {
          opacity: 0.5;
          cursor: not-allowed;
          transform: none !important;
        }

        button:active:not(:disabled) {
          transform: translateY(0);
        }

        /* Loading state */
        .loading::before {
          content: '';
          position: absolute;
          width: 1rem;
          height: 1rem;
          border: 2px solid currentColor;
          border-radius: 50%;
          border-top-color: transparent;
          animation: spin 1s linear infinite;
        }

        @keyframes spin {
          to {
            transform: rotate(360deg);
          }
        }
      </style>
      
      <button 
        class="${this.variant} ${this.size}"
        ?disabled="${this.disabled}"
        aria-label="${this.label}"
      >
        <slot>${this.label}</slot>
      </button>
    `
  }

  setupEventListeners() {
    const button = this.$('button')
    button.addEventListener('click', (e) => {
      if (!this.disabled) {
        this.emit('custom-click', {
          label: this.label,
          variant: this.variant
        })
      }
    })
  }

  /**
   * Handle attribute changes reactively (pure JavaScript!)
   * @param {string} name - Attribute name that changed
   * @param {string|null} oldValue - Previous attribute value
   * @param {string|null} newValue - New attribute value
   */
  attributeChangedCallback(name, oldValue, newValue) {
    if (this._initialized && oldValue !== newValue) {
      this.render()
      this.setupEventListeners()
    }
  }
}

// Register the component
customElements.define('custom-button', ButtonComponent)

Server-Side Rendered Component with Declarative Shadow DOM

src/static/index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Modern Web App</title>
  <style>
    body {
      font-family: system-ui, -apple-system, sans-serif;
      margin: 0;
      padding: 2rem;
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      min-height: 100vh;
    }
  </style>
</head>
<body>
  <h1>Modern Web Components Demo</h1>
  
  <!-- Declarative Shadow DOM example -->
  <custom-button label="Server Rendered">
    <template shadowrootmode="open">
      <style>
        button {
          background: #10b981;
          color: white;
          border: none;
          padding: 1rem 2rem;
          border-radius: 0.5rem;
          cursor: pointer;
          font-size: 1rem;
          transition: transform 0.2s ease;
        }
        button:hover {
          transform: translateY(-2px);
        }
      </style>
      <button>
        <slot>Server Rendered Button</slot>
      </button>
    </template>
  </custom-button>

  <script type="module">
    import './components/ui/button-component.js'
    
    // Progressive enhancement
    document.addEventListener('DOMContentLoaded', () => {
      const buttons = document.querySelectorAll('custom-button')
      buttons.forEach(button => {
        button.addEventListener('custom-click', (e) => {
          console.log('Button clicked:', e.detail)
        })
      })
    })
  </script>
</body>
</html>

⚑ Architect Backend Setup

HTTP Functions Structure

src/http/get-index/index.js

import { readFileSync } from 'fs'
import { join } from 'path'

/**
 * Serve the main application page
 */
export async function handler(req) {
  try {
    const html = readFileSync(
      join(process.cwd(), 'src', 'static', 'index.html'), 
      'utf-8'
    )
    
    return {
      statusCode: 200,
      headers: {
        'content-type': 'text/html; charset=utf8',
        'cache-control': 'no-cache'
      },
      body: html
    }
  } catch (error) {
    return {
      statusCode: 500,
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ error: 'Internal server error' })
    }
  }
}

src/http/get-api-components/index.js

import arc from '@architect/functions'

/**
 * Get all components from database
 */
export async function handler(req) {
  try {
    const data = await arc.tables()
    const result = await data.components.scan({})
    
    return {
      statusCode: 200,
      headers: {
        'content-type': 'application/json',
        'access-control-allow-origin': '*'
      },
      body: JSON.stringify({
        components: result.Items || [],
        count: result.Count || 0
      })
    }
  } catch (error) {
    console.error('Error fetching components:', error)
    return {
      statusCode: 500,
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ error: 'Failed to fetch components' })
    }
  }
}

src/http/post-api-components/index.js

import arc from '@architect/functions'
import { randomUUID } from 'crypto'

/**
 * Create a new component in database
 */
export async function handler(req) {
  try {
    const { name, description, type } = JSON.parse(req.body || '{}')
    
    if (!name || !type) {
      return {
        statusCode: 400,
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({ 
          error: 'Missing required fields: name and type' 
        })
      }
    }
    
    const data = await arc.tables()
    const componentId = randomUUID()
    
    const component = {
      componentId,
      name,
      description: description || '',
      type,
      createdAt: new Date().toISOString(),
      updatedAt: new Date().toISOString()
    }
    
    await data.components.put(component)
    
    return {
      statusCode: 201,
      headers: {
        'content-type': 'application/json',
        'access-control-allow-origin': '*'
      },
      body: JSON.stringify({ component })
    }
  } catch (error) {
    console.error('Error creating component:', error)
    return {
      statusCode: 500,
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ error: 'Failed to create component' })
    }
  }
}

πŸ”„ Agentic Workflow Process

Step-by-Step Development Process

1. Define Feature Requirements

# Example: Add a new Counter Component
echo "Feature: Interactive Counter Component
- Should display current count
- Should have increment/decrement buttons  
- Should emit events on value change
- Should be accessible with ARIA labels
- Should persist state via localStorage" > requirements.md

2. Write Failing Test First

// test/component/counter-component.test.js
import t from 'tap'
// ... DOM setup ...

t.test('CounterComponent', async t => {
  t.test('should initialize with zero count', async t => {
    const counter = document.createElement('counter-component')
    document.body.appendChild(counter)
    
    await customElements.whenDefined('counter-component')
    
    const display = counter.shadowRoot.querySelector('.count-display')
    t.equal(display.textContent, '0', 'should start with zero')
  })
  
  t.test('should increment count when plus button clicked', async t => {
    const counter = document.createElement('counter-component')
    document.body.appendChild(counter)
    
    await customElements.whenDefined('counter-component')
    
    const plusButton = counter.shadowRoot.querySelector('.increment')
    plusButton.click()
    
    const display = counter.shadowRoot.querySelector('.count-display')
    t.equal(display.textContent, '1', 'should increment to 1')
  })
})

3. Run Tests (Should Fail)

npm test
# Expected: Tests fail because CounterComponent doesn't exist yet

4. Implement Minimal Component

// src/components/ui/counter-component.js
import { BaseComponent } from '../base/base-component.js'

export class CounterComponent extends BaseComponent {
  constructor() {
    super()
    this._count = 0
  }

  render() {
    this.shadowRoot.innerHTML = `
      <style>
        /* Minimal styling for tests to pass */
        .counter { display: flex; align-items: center; gap: 1rem; }
        button { padding: 0.5rem 1rem; }
        .count-display { font-size: 1.5rem; font-weight: bold; }
      </style>
      
      <div class="counter">
        <button class="decrement">-</button>
        <span class="count-display">${this._count}</span>
        <button class="increment">+</button>
      </div>
    `
  }

  setupEventListeners() {
    this.$('.increment').addEventListener('click', () => {
      this._count++
      this.render()
      this.setupEventListeners()
    })
    
    this.$('.decrement').addEventListener('click', () => {
      this._count--
      this.render()
      this.setupEventListeners()
    })
  }
}

customElements.define('counter-component', CounterComponent)

5. Run Tests (Should Pass)

npm test
# Expected: Basic tests pass

6. Add More Tests for Edge Cases

t.test('should not go below zero when decrementing', async t => {
  const counter = document.createElement('counter-component')
  document.body.appendChild(counter)
  
  await customElements.whenDefined('counter-component')
  
  const decrementButton = counter.shadowRoot.querySelector('.decrement')
  decrementButton.click() // From 0 to -1, should stay at 0
  
  const display = counter.shadowRoot.querySelector('.count-display')
  t.equal(display.textContent, '0', 'should not go below zero')
})

7. Enhance Implementation

// Add bounds checking to CounterComponent
setupEventListeners() {
  this.$('.increment').addEventListener('click', () => {
    this._count++
    this._updateDisplay()
  })
  
  this.$('.decrement').addEventListener('click', () => {
    if (this._count > 0) {  // Add bounds checking
      this._count--
      this._updateDisplay()
    }
  })
}

_updateDisplay() {
  this.$('.count-display').textContent = this._count
  this.emit('count-changed', { count: this._count })
}

8. Continue Until All Requirements Met

  • βœ… Basic increment/decrement
  • βœ… Bounds checking
  • ⏳ Persistence (next iteration)
  • ⏳ Accessibility improvements
  • ⏳ Styling enhancements

NPM Scripts for Agentic Workflow

{
  "scripts": {
    "test": "tap run",
    "test:watch": "tap run --watch",
    "test:unit": "tap run test/unit/**/*.js",
    "test:component": "tap run test/component/**/*.js",
    "test:integration": "tap run test/integration/**/*.js",
    "dev": "concurrently \"npm run test:watch\" \"arc sandbox\"",
    "coverage": "tap run --coverage-report=html",
    "lint": "eslint src test",
    "deploy:staging": "arc deploy --staging",
    "deploy:production": "arc deploy --production"
  }
}

πŸš€ Production Deployment

Environment Configuration

# Set up AWS credentials
aws configure

# Set Architect app secret for sessions
arc env staging ARC_APP_SECRET $(openssl rand -hex 32)
arc env production ARC_APP_SECRET $(openssl rand -hex 32)

Deployment Commands

# Deploy to staging
npm run deploy:staging

# Run integration tests against staging
ARC_ENV=staging npm run test:integration

# Deploy to production
npm run deploy:production

Performance Monitoring

// src/shared/performance.js
export class PerformanceMonitor {
  static measureWebComponent(componentName, fn) {
    performance.mark(`${componentName}-start`)
    const result = fn()
    performance.mark(`${componentName}-end`)
    performance.measure(
      `${componentName}-duration`,
      `${componentName}-start`,
      `${componentName}-end`
    )
    return result
  }
  
  static getMetrics() {
    return performance.getEntriesByType('measure')
  }
}

⚑ Best Practices

Code Quality

  1. Always write tests first - Follow strict TDD with node-tap
  2. Use Pure JavaScript - ES2023+ with JSDoc for documentation and editor hints
  3. Embrace Web Standards - Prefer native APIs over polyfills
  4. Component Encapsulation - Use Shadow DOM for true style and behavior isolation
  5. No Build Steps - Direct execution in browsers and Node.js
  6. JSDoc Everything - Get TypeScript-level intellisense without compilation

Performance

  1. Lazy Load Components - Import components on-demand
  2. Minimize Shadow DOM Re-renders - Cache DOM references
  3. Use Native ES Modules - Avoid build steps when possible
  4. Optimize Critical Path - Inline critical CSS in component templates

Security

  1. Validate All Inputs - Both client and server side
  2. Use Content Security Policy - Prevent XSS attacks
  3. Sanitize User Content - Never trust user input
  4. Regular Dependency Updates - Keep packages current

Accessibility

  1. Semantic HTML - Use proper HTML elements
  2. ARIA Labels - Enhance screen reader support
  3. Keyboard Navigation - Support all interactions
  4. Color Contrast - Meet WCAG AA standards

Testing

  1. Component Tests First - Focus on component-level testing for maximum value
  2. Mock External Dependencies - Keep tests isolated and fast
  3. Test User Workflows - Write integration tests for critical paths
  4. Maintain High Coverage - Aim for 90%+ with meaningful tests

πŸ“š Additional Resources


🎯 Quick Start Checklist

  • Install Node.js 20+ and npm 10+
  • Clone project template with npm create
  • Set up AWS credentials for Architect
  • Write first failing test
  • Implement minimal feature
  • Run tests until they pass
  • Deploy to staging
  • Run integration tests
  • Deploy to production

Remember: Tests should be empowering and straightforward, reducing cognitive load while giving you confidence to refactor and fix bugs. Every feature should start with a failing test and progress through small, verifiable iterations.


This guide represents current best practices as of 2025. Web standards and frameworks continue to evolve - always verify against official documentation for the latest features and recommendations.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment