The Doh Router seamlessly integrates traditional HTTP handling with Socket.IO, providing a unified interface for both protocols that offers:
Configuration is managed through the pod.yaml
file. Key settings include:
express_config:
# Basic HTTP/HTTPS configuration
hostname: example.com
strict_hostnames: false
port: 3000
ssl_port: 443
ssl_info:
key: /path/to/key.pem
cert: /path/to/cert.pem
# Tunneling configuration
tunnel_ssl_port: 8443
tunnel_remote_url: https://tunnel.example.com:8443
# Host forwarding rules (path patterns to external URLs)
host_forwards:
'/api/external/*': 'https://api.external-service.com'
# Array of paths to be ignored by the default file exposure
ignore_paths:
- .doh
- .git
- pod.yaml
- .env
- '*.env'
# Security configurations
cors_hosts: ['trusted-origin.com']
cors_wildcard: false
only_use_defined_hosts: false
# Content Security Policy settings
helmet: true
content_security_policy: false
use_content_security_policy_defaults: true
# Max age for caching in seconds
cache_max_age: 10
# Maximum size for request bodies
body_size_limit: '50mb'
# Configuration for rate limiting
rate_limit:
windowMs: 900000 # 15 minutes
max: 100 # Limit each IP to 100 requests per windowMs
# Request logging
log_routes: false
New in Doh
The Doh Router now prioritizes file-based routing over code-based route registration. This means:
index.html
), that file is served directly..html
file or any directory containing an index.html
.Router.AddRoute
or similar APIs.This approach enables a more intuitive, filesystem-driven development experience, while still supporting advanced API and dynamic routes via code.
Routing Flow Summary:
The Router includes a built-in analytics system that tracks route usage, performance metrics, and custom events:
// Enable analytics in pod configuration
Doh.Pod('express_router', {
browser_pod: {
analytics: {
enabled: true,
excludePaths: ['/health', '/metrics', '*.js', '*.css', '*.map', '*.ico'],
sampleRate: 1.0
}
}
})
// Register analytics event handlers
Router.onAnalytics('route', function(routeData) {
console.log(`Route accessed: ${routeData.path} (${routeData.method})`);
// Process route analytics data (response time, client info, etc.)
});
Router.onAnalytics('event', function(eventData) {
console.log(`Custom event: ${eventData.event}`);
// Process custom event data
});
// Track custom events from client or server
Router.trackEvent('user_action', {
action: 'click',
elementId: 'submit-button',
page: '/checkout'
});
The analytics system automatically collects:
Doh Router unifies HTTP and Socket.IO routing, allowing a single route definition to handle both protocols seamlessly:
// We depend on express_router to ensure Router is properly initialized
Doh.Module('mymodule_routes', ['express_router'], function(Router) {
Router.AddRoute('/api/data', async function(data, req, res, callback) {
// 'data' contains the request payload, regardless of HTTP method or Socket.IO
// 'req' and 'res' are provided for HTTP requests
// 'callback' is used for Socket.IO responses
const result = await processData(data);
// Use SendJSON for both HTTP and Socket.IO responses
Router.SendJSON(res, result, callback);
});
});
Note: With the new routing priority, code-based route registration is now secondary to file-based routing. If a request matches an HTML file or directory, that file is served first. Only unmatched requests are handled by registered routes.
Best Practice: Use file-based routing for static and template-driven pages, and code-based routes for APIs, dynamic endpoints, or advanced logic.
The Router implements sophisticated conflict detection to prevent ambiguous routing:
// These routes would conflict (exact match)
Router.AddRoute('/users', handleUsers);
Router.AddRoute('/users', handleUsersAgain); // Will log a conflict warning
// These won't conflict (static path vs wildcard)
Router.AddRoute('/users/:id', handleUserById);
Router.AddRoute('/users/profile', handleUserProfile); // Takes precedence due to static path
// These would conflict (wildcards in same position)
Router.AddRoute('/products/:category', handleCategory);
Router.AddRoute('/products/:id', handleProductId); // Will log a conflict warning
The conflict detection follows these rules:
/
) only conflicts with another base pathDoh Router emulates HTTP requests over WebSockets, allowing for protocol-agnostic APIs:
// Client-side (using Socket.IO)
socket.emit('/api/data', { some: 'data' }, (response) => {
console.log(response); // Handled just like an HTTP response
});
// Server-side (same route handling both HTTP and Socket.IO)
Router.AddRoute('/api/data', function(data, req, res, callback) {
// Process data...
Router.SendJSON(res, { result: 'success' }, callback);
});
The socket connection automatically:
The Router provides several methods for sending responses:
// Send JSON response (works for both HTTP and Socket.IO)
Router.SendJSON(res, { result: 'success' }, callback);
// Send HTML response with template processing
Router.SendHTML(res, 'my_module', ['dependency1', 'dependency2'], 'Page Title', 'path/to/template.html', customHeadContent);
// Send raw content with optional templating
Router.SendContent(res, '<h1>Hello World</h1>', {
title: 'Custom Page',
head: '<link rel="stylesheet" href="/styles.css">',
dependencies: ['my_dependency']
});
Doh Router provides advanced forwarding capabilities:
// Simple forwarding
Router.AddHostForward('/api/external/*', 'https://api.example.com');
// Forwarding with request/response interceptor
Router.AddHostForward('/api/transform/*', 'https://backend.example.com',
async function(response, req, res, callback, requestOptions) {
// Transform the response before sending it back
// This could modify headers, data, or perform other operations
return response;
}
);
The tunneling feature enables secure remote access to local development environments:
# Server configuration (public-facing)
express_config:
tunnel_ssl_port: 8443
ssl_info:
key: /path/to/key.pem
cert: /path/to/cert.pem
# Client configuration (private development environment)
express_config:
tunnel_remote_url: https://tunnel.example.com:8443
This creates a secure bidirectional tunnel between environments:
tunnel_ssl_port
tunnel_remote_url
The Router includes comprehensive security features:
// Global middleware for all routes
Router.AddMiddleware('/*', authenticationMiddleware);
// Path-specific middleware
Router.AddMiddleware('/admin/*', [
Router.parseToken,
Router.requireAuth,
adminAccessCheck
]);
// Route-specific middleware
Router.AddRoute('/protected', [Router.parseToken, Router.requireAuth],
function(data, req, res, callback) {
// Protected route logic
Router.SendJSON(res, { protected: 'data' }, callback);
});
Additional security features include:
// Create custom middleware
const loggingMiddleware = (req, res, next) => {
console.log(`Request: ${req.method} ${req.url}`);
const startTime = Date.now();
// Measure response time
res.on('finish', () => {
const duration = Date.now() - startTime;
console.log(`Response: ${res.statusCode} (${duration}ms)`);
});
next();
};
// Apply middleware globally for specific paths
Router.AddMiddleware('/api/*', loggingMiddleware);
// Stack middleware for specific routes
Router.AddRoute('/important', [
loggingMiddleware,
authMiddleware,
validationMiddleware
], handleImportantRoute);
// Forward different API paths to specialized microservices
Router.AddHostForward('/api/users/*', 'https://user-service.internal');
Router.AddHostForward('/api/products/*', 'https://product-service.internal');
Router.AddHostForward('/api/orders/*', 'https://order-service.internal');
// Forward with request transformation
Router.AddHostForward('/legacy/*', 'https://legacy-system.internal',
async function(response, req, res, callback, requestOptions) {
// Convert legacy response format to modern API format
const transformedData = transformLegacyResponse(await response.data);
response.data = transformedData;
return response;
}
);
This creates a protocol-agnostic API that works identically over HTTP and WebSockets, making it ideal for both traditional web applications and real-time interactive experiences.
The Doh Router includes a built-in Hot Module Replacement (HMR) system that enables live updates of HTML content without requiring page refreshes. All HTML files served through file-based routing are automatically equipped with HMR capabilities.
Doh.processHtml
function in the express server automatically injects HMR scripts into all HTML responses:// HMR script injected after <body> tag
const hmrScript = `
<script type="module">
import "${Doh.hostUrlString()}/doh_js/deploy.js";
await Doh.load('hmr');
await Doh.live_html('${DohPath.DohSlash(filepath)}');
</script>`;
When creating custom HTML templates, you don't need to add any special code - the HMR functionality is automatically injected. The system works with:
This creates a streamlined development workflow where changes to HTML files are instantly reflected in the browser.
The Doh Router includes built-in image resizing capabilities that can be accessed through URL parameters. This feature allows for dynamic image resizing without requiring pre-processing or storing multiple versions of the same image.
Image size presets are configured in the pod.yaml
file under express_config
:
express_config:
image_size_presets:
icon: { width: 32, height: 32, fit: 'cover' }
'small-thumb': { width: 64, height: 64, fit: 'cover' }
'large-thumb': { width: 128, height: 128, fit: 'cover' }
smaller: { width: 240, fit: 'inside' }
small: { width: 320, fit: 'inside' }
medium: { width: 640, fit: 'inside' }
large: { width: 1024, fit: 'inside' }
Each preset can specify:
width
: Target width in pixelsheight
: Target height in pixels (optional)fit
: Resize fitting method ('cover', 'contain', 'inside', 'outside', 'fill')To resize an image, simply add the size
parameter to the image URL:
<!-- Original image -->
<img src="/images/photo.jpg">
<!-- Resized versions -->
<img src="/images/photo.jpg?size=icon">
<img src="/images/photo.jpg?size=small-thumb">
<img src="/images/photo.jpg?size=large-thumb">
<img src="/images/photo.jpg?size=small">
<img src="/images/photo.jpg?size=medium">
<img src="/images/photo.jpg?size=large">
The image resizing feature supports the following formats:
You can extend or override the default size presets in your pod.yaml
:
express_config:
image_size_presets:
# Override existing presets
icon: { width: 48, height: 48, fit: 'cover' }
# Add new presets
'hero': { width: 1920, height: 1080, fit: 'cover' }
'card': { width: 400, height: 300, fit: 'cover' }
'avatar': { width: 150, height: 150, fit: 'cover' }
# Custom aspect ratio
'banner': { width: 1200, height: 400, fit: 'cover' }
# Maintain aspect ratio
'sidebar': { width: 300, fit: 'inside' }
<!-- Responsive images with different sizes -->
<picture>
<source media="(max-width: 320px)" srcset="/images/hero.jpg?size=small">
<source media="(max-width: 640px)" srcset="/images/hero.jpg?size=medium">
<source media="(max-width: 1024px)" srcset="/images/hero.jpg?size=large">
<img src="/images/hero.jpg?size=hero" alt="Hero image">
</picture>
<!-- Thumbnail grid -->
<div class="thumbnails">
<img src="/images/item1.jpg?size=small-thumb" alt="Item 1">
<img src="/images/item2.jpg?size=small-thumb" alt="Item 2">
<img src="/images/item3.jpg?size=small-thumb" alt="Item 3">
</div>
<!-- User avatars -->
<img src="/images/avatar.jpg?size=icon" alt="User avatar">
This feature provides a flexible and efficient way to handle image resizing without requiring pre-processing or storing multiple versions of the same image.
The route conflict detection system in Doh identifies potential conflicts between registered* routes while allowing for flexible and intuitive URL structuring. It prevents ambiguous routing scenarios while permitting rich, nested route structures.
*file-based routes always supercede registered routing
When adding a new route, the system checks it against all existing routes to detect any conflicts. If a conflict is found, the system prevents the new route from being added and reports the conflict.
Exact Match Conflict
Static Part Precedence
Wildcard Conflict
*
and parameter placeholders (e.g., :id
).Longer Path Precedence
Base Path Conflict
/
) only conflicts with another base path (/
)./users
vs /users
/users
vs /users/profile
/users/*
vs /users/account
/users/:id
vs /users/*
/users/:id/posts
vs /users/*/comments
/users/profile
vs /users/:id
/users/*
vs /users/*/settings
/users/:id/profile
vs /users/*/settings
/
vs /users
/users/:id
vs /users/:name
/products/:category/*
vs /products/:id
/blog/:year/:month
vs /blog/:slug
/api/v1/*
vs /api/v2/*
/search
vs /search/:query
/items/:id
vs /items/new
/users/:id/posts/:postId
vs /users/active/posts/*
:id
)/products/:category/:id
vs /products/featured/:id
:category
)/articles/*
vs /articles/:id/edit
/users/*
vs /users/:id/profile
/api/v1/:resource/*
vs /api/v1/:resource/:id
/
vs /