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:

  1. Starts at the requested file's directory
  2. Searches up the tree for a dhandler file
  3. Executes the first dhandler found
  4. 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:

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:

Combining Dhandlers and Autohandlers

Dhandlers are also wrapped by autohandlers:

This means your API can have:

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

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);
%>