We present DOM-Aware Primitives, a client-side JavaScript library that extends HTML elements with HTTP methods (GET, POST, PUT, DELETE) to enable direct communication with DOM-Aware Servers (DAS). This approach treats HTML documents as both user interfaces and APIs, using CSS selectors as resource identifiers and standard HTTP methods for state manipulation. The architecture includes a novel capability discovery mechanism using OPTIONS requests with Range headers, enabling progressive user interfaces that adapt to user permissions in real-time. By eliminating the traditional separation between HTML for humans and JSON APIs for machines, this architecture simplifies web development while enabling new patterns for real-time collaboration, progressive enhancement, and semantic web applications.
Modern web architecture typically separates presentation (HTML) from data exchange (JSON APIs), requiring developers to maintain parallel structures for human and machine consumption. This separation introduces complexity, synchronization challenges, and violates the original principles of REST as described by Fielding [1].
DOM-Aware Primitives proposes a fundamental shift: HTML elements become directly addressable and mutable resources, accessible through standard HTTP methods. This approach treats the DOM as a hypermedia-driven state engine where each element can respond to HTTP operations, effectively making every HTML document a self-describing API.
Traditional web applications maintain separate layers:
This separation creates several problems:
Fielding’s REST dissertation emphasized hypermedia as the engine of application state (HATEOAS) [1]. However, most “RESTful” APIs today:
DOM-Aware Primitives returns to these original principles by using HTML as the hypermedia format, where links and forms provide state transitions, and elements themselves become resources.
The architecture rests on four key principles:
The client library extends HTMLElement.prototype
with HTTP methods:
element.GET() // Retrieve element state from server
element.PUT() // Send element's current state to server
element.POST() // Append new child element (equivalent to appendChild)
element.DELETE() // Remove element from document
element.HEAD() // Check element metadata
Each operation includes a Range: selector=<css-selector>
header, allowing the server to identify the target element. The selector generation algorithm prioritizes stability:
#elementId
)#parentId > div:nth-child(2)
)The Range header is traditionally used in HTTP to request partial content from resources, typically for resumable downloads or streaming media. DOM-Aware Primitives repurposes this standard header with a custom range unit: selector
.
GET /video.mp4 HTTP/1.1
Range: bytes=200-1023
PUT /page.html HTTP/1.1
Range: selector=#post-123-title
Content-Type: text/html
<h1>Updated Blog Post Title</h1>
This approach offers several advantages:
// Simple ID selector
document.querySelector('#header').PUT()
// Sends: Range: selector=#header
// Contextual selector with parent ID
document.querySelector('#content > p:first-child').DELETE()
// Sends: Range: selector=#content > p:first-child
// Complex selector for deeply nested elements
document.querySelector('.article .comments li:nth-child(3)').GET()
// Sends: Range: selector=.article .comments li:nth-child(3)
DOM-Aware Servers must:
206 Partial Content
for successful range operations416 Range Not Satisfiable
if selector matches no elements400 Bad Request
for invalid selectorsExample server pseudocode:
def handle_request(request):
if 'Range' in request.headers:
range_value = request.headers['Range']
if range_value.startswith('selector='):
selector = range_value[9:] # Remove 'selector=' prefix
elements = dom.select(selector)
if not elements:
return Response(416, "Range Not Satisfiable")
# Process the matched elements...
DOM-Aware Servers must:
Accept-Ranges: selector
headerMultiple users can edit different document sections simultaneously without custom synchronization protocols:
// User A modifies and saves a paragraph
const paragraph = document.querySelector('#intro p')
paragraph.textContent = 'Updated introduction text'
await paragraph.PUT()
// User B appends a new comment
document.querySelector('#comments').POST('<div class="comment">New comment</div>')
The server handles conflict resolution at the element level rather than document level, enabling fine-grained collaboration.
Applications can enhance functionality based on server capabilities:
document.addEventListener('DASAvailable', () => {
// Enable auto-save for editable content
document.querySelectorAll('[contenteditable]').forEach(el => {
el.addEventListener('blur', () => el.PUT())
})
})
With microdata or RDFa markup, elements carry semantic meaning:
<div itemscope itemtype="http://schema.org/Person">
<span itemprop="name">John Doe</span>
<span itemprop="email">john@example.com</span>
</div>
Agents can understand and manipulate structured data without custom APIs.
CMSs can expose editing capabilities directly through HTML:
// Enable save-on-blur for existing contenteditable elements
document.addEventListener('DASAvailable', () => {
document.querySelectorAll('[contenteditable]').forEach(el => {
el.addEventListener('blur', async () => {
try {
await el.PUT()
// Visual feedback on successful save
el.style.outline = '2px solid green'
setTimeout(() => el.style.outline = '', 1000)
} catch (error) {
el.style.outline = '2px solid red'
console.error('Failed to save:', error)
}
})
})
})
// CMS edit mode toggle example
let dasAvailable = false
document.addEventListener('DASAvailable', () => { dasAvailable = true })
function toggleEditMode(enabled) {
if (enabled && dasAvailable) {
// Make article elements editable
document.querySelectorAll('article h1, article h2, article p').forEach(el => {
el.contentEditable = 'plaintext-only'
el.addEventListener('blur', () => el.PUT(), { once: true })
})
// Show edit UI
document.body.classList.add('edit-mode')
} else {
// Disable editing
document.querySelectorAll('[contenteditable]').forEach(el => {
el.contentEditable = 'false'
})
document.body.classList.remove('edit-mode')
}
}
The architecture leverages standard HTTP security mechanisms:
This approach ensures that security implementations remain consistent with existing web standards while keeping sensitive logic on the server where it belongs.
Traditional web applications often expose all interface elements regardless of user permissions, relying on server-side rejection of unauthorized actions. This approach leads to poor user experience when users attempt operations they cannot perform. DOM-Aware Primitives introduces a capability discovery mechanism that enables truly progressive user interfaces.
The HTTP OPTIONS method, traditionally used to discover allowed methods for entire resources, gains new power when combined with Range headers:
OPTIONS /document.html HTTP/1.1
Range: selector=#delete-button
The server response indicates which methods are permitted for that specific element:
HTTP/1.1 200 OK
Allow: GET, POST
Accept-Ranges: selector
This fine-grained capability discovery enables clients to understand permissions at the element level rather than the document level, providing unprecedented granularity in authorization feedback.
The library includes an <http-can>
Web Component that leverages this capability discovery to conditionally display interface elements:
<http-can method="DELETE" selector="#comment-42">
<button onclick="document.querySelector('#comment-42').DELETE()">
Delete Comment
</button>
</http-can>
The component:
This declarative approach to permission-based UI has several advantages:
This capability discovery mechanism represents a significant advancement in web application security and usability:
Principle of Least Privilege in UI: By showing only permitted actions, interfaces naturally implement the principle of least privilege at the presentation layer.
Self-Documenting Interfaces: The UI itself becomes a form of documentation, accurately reflecting the current user’s capabilities without external documentation.
Dynamic Permission Models: As permissions change (through role updates, time-based access, or contextual factors), the UI automatically adapts without client-side updates.
Reduced Attack Surface: Hidden elements cannot be manipulated through browser developer tools or automated scripts, as they never reach the client.
The http-can component implements several optimizations:
This capability discovery system transforms authorization from a hidden backend concern into a first-class architectural component that directly improves user experience while maintaining security.
While the client library uses standard web APIs, older browsers may require polyfills for:
DOM-Aware Servers must:
Each element operation requires an HTTP request, though this can be mitigated through:
Several projects explore similar concepts:
DOM-Aware Primitives differs by implementing standard HTTP semantics at the element level rather than through custom attributes or protocols.
Working toward web standards adoption could involve:
Development tools could include:
Future capabilities might include:
DOM-Aware Primitives represents a return to the web’s foundational principles while addressing modern development needs. By treating HTML elements as HTTP-accessible resources, we eliminate the artificial separation between human and machine interfaces. The capability discovery mechanism through OPTIONS requests enables interfaces that dynamically adapt to user permissions, implementing security through opacity while improving user experience. This approach simplifies development, enables new collaborative patterns, and provides a path toward truly semantic web applications.
The architecture demonstrates that powerful web applications need not require complex API layers. Instead, by embracing HTML as a hypermedia format, implementing proper REST constraints, and leveraging HTTP’s built-in capability discovery mechanisms, we can build systems that are simultaneously simpler, more secure, and more capable than current approaches.
[1] Fielding, R. T. (2000). Architectural styles and the design of network-based software architectures (Doctoral dissertation, University of California, Irvine).
[2] htmx - high power tools for html. https://htmx.org/
[3] Hotwire: HTML Over The Wire. https://hotwired.dev/
[4] Alpine.js: A rugged, minimal framework for composing JavaScript behavior in your markup. https://alpinejs.dev/
// Retrieve updated content from server
await element.GET()
// Append new child element (string, HTMLElement, or DocumentFragment)
await element.POST('<div>New content</div>')
// or
const newEl = document.createElement('div')
await element.POST(newEl)
// Send element's current state to server
await element.PUT()
// Remove element
await element.DELETE()
// Check element metadata
await element.HEAD()
element.addEventListener('DASOk', (event) => {
console.log('Operation successful:', event.detail)
})
element.addEventListener('DASError', (event) => {
console.error('Operation failed:', event.detail)
})
// Base HTML works without JavaScript
// <a href="/delete-item?id=123" class="delete-link">Delete</a>
// Enhance with DOM-Aware functionality when available
document.addEventListener('DASAvailable', () => {
// Replace traditional form/link behavior with direct element operations
document.querySelectorAll('.delete-link').forEach(link => {
link.addEventListener('click', async (e) => {
e.preventDefault() // Stop navigation
// Find the item to delete (e.g., parent <li>)
const item = link.closest('li')
try {
await item.DELETE()
// Item removed by server, DOM updated automatically
} catch (error) {
// Fall back to traditional navigation if DELETE fails
window.location.href = link.href
}
})
})
})
// Example: Enhance a todo list that works without JavaScript
document.addEventListener('DASAvailable', () => {
const todoList = document.querySelector('#todo-list')
// Convert form submission to DOM-aware POST
const addForm = document.querySelector('#add-todo-form')
addForm.addEventListener('submit', async (e) => {
e.preventDefault()
const input = addForm.querySelector('input[name="task"]')
const newItem = `<li>${input.value}</li>`
try {
await todoList.POST(newItem)
input.value = '' // Clear input on success
} catch (error) {
// Fall back to form submission
addForm.submit()
}
})
})
// Auto-save content on blur
document.querySelectorAll('[contenteditable]').forEach(editor => {
editor.addEventListener('blur', async () => {
try {
await editor.PUT()
editor.classList.add('saved')
} catch (error) {
editor.classList.add('error')
}
})
})
// Track changes and sync
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
if (mutation.type === 'characterData' || mutation.type === 'childList') {
mutation.target.PUT()
}
})
})
observer.observe(document.querySelector('#collaborative-doc'), {
childList: true,
characterData: true,
subtree: true
})
Request:
OPTIONS / HTTP/1.1
Host: example.com
Response:
HTTP/1.1 200 OK
Accept-Ranges: selector
Allow: GET, HEAD, POST, PUT, DELETE, OPTIONS
Client Code:
document.querySelector('#sidebar').GET()
HTTP Request:
GET /page.html HTTP/1.1
Host: example.com
Range: selector=#sidebar
HTTP Response:
HTTP/1.1 206 Partial Content
Content-Type: text/html
Content-Range: selector #sidebar
<div id="sidebar">
<h3>Navigation</h3>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
</ul>
</div>
Client Code:
const title = document.querySelector('h1')
title.textContent = 'New Page Title'
await title.PUT()
HTTP Request:
PUT /page.html HTTP/1.1
Host: example.com
Range: selector=h1
Content-Type: text/html
<h1>New Page Title</h1>
HTTP Response:
HTTP/1.1 204 No Content
Client Code:
const list = document.querySelector('#todo-list')
await list.POST('<li>New todo item</li>')
HTTP Request:
POST /page.html HTTP/1.1
Host: example.com
Range: selector=#todo-list
Content-Type: text/html
<li>New todo item</li>
HTTP Response:
HTTP/1.1 201 Created
Location: #todo-list > li:last-child
Client Code:
document.querySelector('.advertisement').DELETE()
HTTP Request:
DELETE /page.html HTTP/1.1
Host: example.com
Range: selector=.advertisement
HTTP Response:
HTTP/1.1 204 No Content
Selector Not Found:
HTTP/1.1 416 Range Not Satisfiable
Content-Range: selector */0
Invalid Selector:
HTTP/1.1 400 Bad Request
Content-Type: text/plain
Invalid CSS selector: "#unclosed
Unauthorized Operation:
HTTP/1.1 403 Forbidden
Content-Type: text/plain
User lacks permission to modify element #admin-panel