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
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
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
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' })
}
}
}
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/
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>
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()
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)
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')
})
})
-
Organize by Domain: Group modules logically (
components/
,utils/
,services/
) -
Use Trailing Slashes: Enable directory imports (
"utils/": "/modules/utils/"
) -
Version CDN Dependencies: Pin versions for stability (
@^3.0.0
) -
Cache Static Modules: Use long cache headers for
/modules/*
routes -
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>
- 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! π
- Project Architecture
- Development Environment Setup
- Testing Strategy
- Web Components Implementation
- ES Modules Import Maps
- Architect Backend Setup
- Agentic Workflow Process
- Production Deployment
- Best Practices
- 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
- 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
- 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!
# Required versions
node --version # v20.0.0+
npm --version # v10.0.0+
# 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
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
.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
Follow the "Testing Diamond" approach - focus primarily on component tests that provide high confidence with realistic scenarios
- Unit Tests (20%): Test pure functions and utilities
- Component Tests (70%): Test Web Components in isolation
- Integration Tests (10%): Test full user workflows
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')
})
})
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')
})
})
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)
}
}
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)
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>
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' })
}
}
}
# 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
// 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')
})
})
npm test
# Expected: Tests fail because CounterComponent doesn't exist yet
// 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)
npm test
# Expected: Basic tests pass
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')
})
// 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 })
}
- β Basic increment/decrement
- β Bounds checking
- β³ Persistence (next iteration)
- β³ Accessibility improvements
- β³ Styling enhancements
{
"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"
}
}
# 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)
# 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
// 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')
}
}
- Always write tests first - Follow strict TDD with node-tap
- Use Pure JavaScript - ES2023+ with JSDoc for documentation and editor hints
- Embrace Web Standards - Prefer native APIs over polyfills
- Component Encapsulation - Use Shadow DOM for true style and behavior isolation
- No Build Steps - Direct execution in browsers and Node.js
- JSDoc Everything - Get TypeScript-level intellisense without compilation
- Lazy Load Components - Import components on-demand
- Minimize Shadow DOM Re-renders - Cache DOM references
- Use Native ES Modules - Avoid build steps when possible
- Optimize Critical Path - Inline critical CSS in component templates
- Validate All Inputs - Both client and server side
- Use Content Security Policy - Prevent XSS attacks
- Sanitize User Content - Never trust user input
- Regular Dependency Updates - Keep packages current
- Semantic HTML - Use proper HTML elements
- ARIA Labels - Enhance screen reader support
- Keyboard Navigation - Support all interactions
- Color Contrast - Meet WCAG AA standards
- Component Tests First - Focus on component-level testing for maximum value
- Mock External Dependencies - Keep tests isolated and fast
- Test User Workflows - Write integration tests for critical paths
- Maintain High Coverage - Aim for 90%+ with meaningful tests
- Web Components Documentation
- Architect Framework Docs
- node-tap Documentation
- Modern JavaScript Testing Best Practices
- 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.