Home / Articles / Inside DenoX: File Routing, MVC Slices and an AGENTS.md Contract on Deno

This article is published in English.

Inside DenoX: File Routing, MVC Slices and an AGENTS.md Contract on Deno

How the DenoX framework combines Hono, file-based routing, feature slices, global security middleware and a spec-first AGENTS.md workflow for AI coding agents.

1560 words

Wiring up a server is rarely the valuable part of a backend project; shipping features is. Rails, Laravel and Next.js all won developers by making the plumbing decisions for them, and Deno, with its secure-by-default permissions, native TypeScript and built-in tooling, is a natural candidate for the same treatment. DenoX is an open-source full-stack framework for Deno, built on Hono, that tries to be exactly that opinionated layer. Its answers to recurring structural questions, and the way it writes them down for AI coding agents, are patterns you can reuse in any TypeScript backend.

The gap an opinionated layer fills

Deno 2 brought npm compatibility, the JSR registry, a mature standard library and a single binary that can lint, format, test, compile and bundle (see how Deno 2.x solved Node compatibility and tooling fatigue for background). What a bare runtime cannot give you is agreement on the questions every team argues about: where business logic belongs, how errors are reported, who validates configuration and where rate limiting sits.

DenoX answers them with a single principle: convention over configuration, verified by tooling. Documented conventions drift; conventions checked in CI hold.

File-based routing with a generated, committed table

Routes come from the file system. Adding a file under the pages directory adds a URL, with bracketed segments becoming parameters:

src/frontend/pages/
├── index.ts            →  /
├── about/main.ts       →  /about
├── users/main.ts       →  /users
└── posts/[id].ts       →  /posts/:id

Discovery is not done at runtime. Running deno task routes walks the tree and emits a static, deterministic route table. Two details make this robust:

  • Static routes are always registered ahead of dynamic ones, so /users/new can never be swallowed by /users/:id. With first-match routers like Hono, registration order is behavior, and generating it removes a classic source of subtle bugs.
  • The generated file is committed to the repository, and CI fails if it is out of date.

Pages are plain functions

A page is an ordinary TypeScript module. It imports Hono's Context type and an HTML escaping helper:

import type { Context } from "hono";
import { escapeHtml } from "@/shared/html.ts";

It then exports a config object selecting a layout and a default function that returns an HTML string:

export const config = { layout: "default" } as const;export default function homePage(c: Context): string {
  const name = escapeHtml(c.req.query("name") ?? "world");
  return `<h1>Hello, ${name}!</h1>`;
}

The detail worth noticing is escapeHtml. The name query parameter is user-controlled, and interpolating it directly into markup would be a textbook reflected XSS hole. In DenoX, escaping untrusted input is not a recommendation but a rule written into the project's engineering contract. Since pages return raw strings with no auto-escaping template engine, every interpolation of external data must go through the helper.

Feature slices with fixed anatomy

On the API side, every feature is a self-contained slice with the same set of files, each with a single job:

src/api/users/
├── user.model.ts        entities only
├── user.dto.ts          unknown → typed DTO (boundary validation)
├── user.repository.ts   interface + default implementation
├── user.service.ts      business rules only — no HTTP, no HTML
├── user.controller.ts   HTTP adapter only
└── user.routes.ts       composition root (constructor injection)

The DTO module turns unknown input into a typed object at the boundary, so nothing deeper in the stack deals with raw request bodies. Services contain business rules only and know nothing about HTTP or HTML. Controllers are thin HTTP adapters. The routes file is the composition root where dependencies are wired through constructor injection.

Services depend on repository interfaces rather than concrete classes. Replacing the in-memory store with Postgres or Deno KV therefore means changing one file per feature.

Errors as typed exceptions

Business rules signal failure by throwing typed exceptions. The service method below refuses to create a second user with an existing email:

async create(dto: CreateUserDto): Promise<User> {
  const existing = await this.repository.findByEmail(dto.email);
  if (existing !== null) {
    throw new ConflictException(`Email "${dto.email}" is already registered`);
  }
  return await this.repository.create(dto);
}

The service does not choose a status code or format a response. A single centralized error handler maps each exception type to a consistent JSON envelope and makes sure stack traces never reach clients. Note that echoing the email back enables account enumeration, which you may want to avoid on public endpoints.

Security implemented once, applied everywhere

Cross-cutting protections live in global middleware rather than in each feature: a Content Security Policy, hardened response headers, CORS rules, origin-based CSRF checks, rate limits keyed by client IP, caps on body size, timeouts and masking of internal errors. Features use them rather than re-implementing them.

Configuration gets the same treatment. Every environment variable is parsed, validated and frozen when the process starts, and the app refuses to boot if anything is missing or malformed. In production, CORS_ORIGIN=* is rejected outright. Failing fast at startup beats debugging a half-configured service in production.

Three layers of tests behind one command

The testing setup goes beyond a placeholder assertion:

  • Unit tests cover pure logic using call-recording mocks and need no Deno permissions at all.
  • Integration tests exercise the fully wired app via app.request(), asserting status codes, response envelopes and even security headers, without opening a socket.
  • End-to-end tests start a real Deno.serve on an ephemeral port and hit it with real fetch calls, including one that deliberately trips the rate limiter to confirm it returns 429.

The complete quality gate, covering formatting, linting, the stale route table check, strict type checking and all test layers, runs with deno task ci, and the GitHub Actions pipeline executes exactly that sequence.

One deploy command, no credential handling

The repository includes manifests for Fly.io, Railway, Render, Docker and a hardened systemd unit for a VPS, plus first-class support for Deno Deploy. A single task lists targets, prints a dry run or executes a deployment:

deno task deploy            # list targets
deno task deploy fly        # dry run: steps + env reminders
deno task deploy fly --run  # execute (auth delegated to the platform CLI)

The deploy tool deliberately never handles credentials. It checks prerequisites, shows the plan along with reminders about required environment variables, and leaves authentication to each platform's official CLI. Secrets stay out of the framework entirely.

AGENTS.md as an enforceable engineering contract

The most distinctive part of DenoX is an AGENTS.md file at the root of the repository, written as the authoritative contract for both human contributors and AI coding agents. It pins the technology stack, defines the canonical directory tree and lists the shared primitives that must never be reinvented: the logger, the exception hierarchy, the response envelope and the configuration module.

It also encodes a Specification Driven Development workflow:

  1. A spec such as specs/feature.md is written with status: draft.
  2. A human reviews it and changes the status to status: approved.
  3. Only after approval does work proceed through architecture, plan, implementation, tests and documentation.

Agents are told explicitly to stop once the spec is written and wait for a human to approve it, so an agent cannot approve its own plan and then rewrite half the codebase. A complete reference cycle for user management shows agents the pattern, and CI enforces the conventions mechanically, down to failing the build when a generated file has been edited by hand.

As agents write more code, conventions matter only as far as they can be checked automatically; versioning the contract next to the code and backing it with CI turns guidance into guardrails. For a related take on instruction files for assistants, see Vercel's AGENTS.md skill for React best practices.

Running it locally

Clone the repository, create an environment file from the example and start the dev server:

git clone https://github.com/olavomello/denox.git
cd denox
cp .env.example .env
deno task dev

Then open http://localhost:8000, call /api/users, send invalid data on purpose and check that the error envelope stays clean and free of stack traces. A live version is also available. The project is MIT licensed; its stated roadmap includes Deno KV and Postgres adapters, automatic layout registration, a dedicated CLI, an authentication module and OpenAPI generation, each planned to go through the same spec-first workflow. Check the repository for its current state before adopting it.

Key takeaways

  • Generate route tables at build time, register static routes before dynamic ones, commit the output and let CI reject stale files.
  • Give each feature a fixed anatomy: boundary DTOs, interface-based repositories, HTTP-free services and thin controllers.
  • Throw typed exceptions and translate them in one place so responses are consistent and never leak stack traces.
  • Put security in global middleware and validate configuration at startup, refusing unsafe values like a wildcard CORS origin in production.
  • Write an AGENTS.md that requires human approval of specs, and enforce its rules in CI so they bind agents and humans alike.