Dhandlers - Dynamic Routing
Dhandlers (default handlers) are special components that handle requests for non-existent files. They're perfect for creating REST APIs, dynamic content generation, and custom routing.
What is a Dhandler?
A dhandler is a file named dhandler (with no extension) that catches requests for missing files in its directory tree. When a file isn't found, StoneJS searches up the directory tree for the nearest dhandler.
How Dhandlers Work
When a requested file doesn't exist, StoneJS:
- Starts at the requested file's directory
- Searches up the tree for a
dhandlerfile - Executes the first dhandler found
- Passes the remaining path to the dhandler
File Type Behavior
Dhandlers are invoked differently based on file extension:
| File Type | Extensions | Dhandler Behavior |
|---|---|---|
| Interpreted | .htm, .html, .css, .js | Invoked if file not found |
| Binary | .png, .pdf, .xls, .xlsx | Invoked if file not found |
| Other | All others | 404 error (no dhandler) |
Creating a Dhandler
Create a file named dhandler in any directory:
<%
// Access the requested path
const requestedPath = $context.dhandlerPath;
const pathParts = requestedPath.split('/').filter(p => p);
// Custom routing logic
if (pathParts.length === 0) {
%>
<h1>Default Page</h1>
<p>No specific file requested.</p>
<%
} else {
%>
<h1>Dynamic Route: <%= requestedPath %></h1>
<p>This was handled by the dhandler!</p>
<%
}
%>
The $context.dhandlerPath Variable
When a dhandler is invoked, $context.dhandlerPath contains the path that triggered it (relative to the dhandler's directory).
Example:
If you have /pages/api/dhandler and someone requests /api/users/123:
$context.dhandlerPath="users/123"$context.componentPath="/full/path/to/pages/api/dhandler"
REST API Example
Dhandlers are perfect for building REST APIs:
<%
// /pages/api/dhandler
const path = $context.dhandlerPath;
const parts = path.split('/').filter(p => p);
const method = $req.method;
// Set JSON response
$res.setHeader('Content-Type', 'application/json');
// Router
if (parts[0] === 'users') {
if (method === 'GET' && parts.length === 1) {
// GET /api/users - List users
$res.send(JSON.stringify({
users: [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' }
]
}));
} else if (method === 'GET' && parts.length === 2) {
// GET /api/users/123 - Get user
const userId = parts[1];
$res.send(JSON.stringify({
id: userId,
name: 'User ' + userId
}));
} else if (method === 'POST') {
// POST /api/users - Create user
$res.send(JSON.stringify({
success: true,
message: 'User created'
}));
}
} else {
// 404
$res.status(404).send(JSON.stringify({
error: 'Not found'
}));
}
%>
Dynamic File Generation
Generate files on-the-fly (PDFs, Excel, images):
<%
// /pages/reports/dhandler
const reportName = $context.dhandlerPath;
if (reportName.endsWith('.pdf')) {
// Generate PDF
$res.setHeader('Content-Type', 'application/pdf');
$res.setHeader('Content-Disposition',
'attachment; filename="' + reportName + '"');
// Use a PDF library to generate content
const pdfContent = generatePDF(reportName);
$res.send(pdfContent);
} else if (reportName.endsWith('.xlsx')) {
// Generate Excel
$res.setHeader('Content-Type',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
const excelContent = generateExcel(reportName);
$res.send(excelContent);
}
%>
Custom 404 Pages
Create a friendly 404 page with a root-level dhandler:
<%
// /pages/dhandler
$res.status(404);
%>
<div class="error-page">
<h1>Page Not Found</h1>
<p>The page <code><%= $req.path %></code> could not be found.</p>
<p><a href="/">Go Home</a></p>
</div>
Try It Out
This demo has a dhandler in the /demo/api directory. Try these URLs:
- /demo/api/users - List all users
- /demo/api/users/123 - Get specific user
- /demo/api/products - List products
- /demo/api/invalid - Invalid endpoint
Combining Dhandlers and Autohandlers
Dhandlers are also wrapped by autohandlers:
This means your API can have:
- An autohandler for authentication
- A dhandler for routing
- Consistent error handling and formatting
Advanced Routing Example
<%
const path = $context.dhandlerPath;
const parts = path.split('/').filter(p => p);
// Blog post URL pattern: /blog/2024/01/15/post-slug
if (parts[0] === '2024' && parts.length === 4) {
const [year, month, day, slug] = parts;
// Fetch blog post from database
const post = await $db.query(
'SELECT * FROM posts WHERE slug = $1 AND date = $2',
[slug, `${year}-${month}-${day}`]
);
if (post.length > 0) {
%>
<article>
<h1><%= post[0].title %></h1>
<time><%= year %>-<%= month %>-<%= day %></time>
<div><%- post[0].content %></div>
</article>
<%
} else {
$res.status(404).send('Post not found');
}
}
%>
Best Practices
- Use dhandlers for dynamic routing, not static content
- Set appropriate HTTP status codes (404, 200, etc.)
- Set correct Content-Type headers
- Validate input from
$context.dhandlerPath - Consider security when parsing paths
- Don't put dhandlers in the root unless you want to catch everything
Debugging Dhandlers
Use context information to debug dhandler behavior:
<%
console.log('Dhandler triggered!');
console.log('Requested path:', $context.dhandlerPath);
console.log('Dhandler file:', $context.componentPath);
console.log('Request method:', $req.method);
console.log('Query params:', $req.query);
%>