This guide covers how to properly create multiple URL paths that serve the same functionality while maintaining clean architecture.
When you need both /views/feature/:id
and /feature/:id
to work identically, serving the same handler with the same authentication and behavior.
// DON'T DO THIS - violates architectural separation
import { featureHandler } from "./backend/controllers/feature.controller.ts";
app.get("/feature/:id", featureHandler);
// DON'T DO THIS - creates /feature/feature/:id instead of /feature/:id
app.route("/feature", viewRoutes); // where viewRoutes has app.get("/feature/:id", ...)
// DON'T DO THIS - inconsistent auth between routes
app.use("/views/*", authCheck);
// Missing: app.use("/feature/*", authCheck);
app.route("/feature", featureRoutes);
First, understand the current route setup:
# Check existing routes file cat /backend/routes/views/_views.routes.ts # Check main router setup cat /main.tsx
Look for:
app.get("/feature/:id", handler)
)Create a new route module that mirrors the existing functionality:
// Create /backend/routes/feature/_feature.routes.ts
import { Hono } from "npm:hono@3.12.12";
import { featureHandler } from "../../controllers/feature.controller.ts";
const app = new Hono();
// Use /:id (not /feature/:id) since it will be mounted at /feature
app.get("/:id", featureHandler);
export default app;
Key Points:
/:id
not /feature/:id
(mounting handles the prefix)_feature.routes.ts
)// Create /backend/routes/feature/README.md
# Feature Routes
This directory contains routes for the feature functionality.
## Routes
### GET /:id
- **Purpose**: [Copy purpose from original route docs]
- **Authentication**: Required (Google OAuth)
- **Parameters**: `id` - [Parameter description]
- **Response**: [Response description]
This is the same functionality as `/views/feature/:id` but mounted at `/feature/:id` for convenience.
Add the new route module to main.tsx:
// Add import
import featureRoutes from "./backend/routes/feature/_feature.routes.ts";
// Add authentication middleware (same as original)
app.use("/feature/*", authCheck);
// Mount routes
app.route("/feature", featureRoutes);
Critical Order:
Ensure you understand the path composition:
Main router mount: app.route("/feature", featureRoutes)
Route definition: app.get("/:id", handler)
Final URL: /feature/:id ✅
NOT:
Route definition: app.get("/feature/:id", handler)
Final URL: /feature/feature/:id ❌
Test that both routes work identically:
# Test original route curl /views/feature/test-id # Test new route curl /feature/test-id # Both should return identical responses (login page if not authenticated)
backend/routes/
├── views/_views.routes.ts # Original route
├── feature/
│ ├── _feature.routes.ts # New duplicate route
│ └── README.md # Documentation
import { Hono } from "npm:hono@3.12.12";
import { lastlogin } from "https://esm.town/v/stevekrouse/lastlogin_safe";
// Import route modules (NOT controllers)
import viewRoutes from "./backend/routes/views/_views.routes.ts";
import featureRoutes from "./backend/routes/feature/_feature.routes.ts";
import authCheck from "./backend/routes/authCheck.ts";
const app = new Hono();
// Apply authentication to both routes
app.use("/views/*", authCheck);
app.use("/feature/*", authCheck);
// Mount routes
app.route("/views", viewRoutes);
app.route("/feature", featureRoutes);
export default lastlogin(app.fetch);
import { Hono } from "npm:hono@3.12.12";
import { featureHandler } from "../../controllers/feature.controller.ts";
const app = new Hono();
app.get("/:id", featureHandler);
export default app;
This approach ensures clean, maintainable code while providing the flexibility of multiple URL paths for the same functionality.
Create a duplicate route that serves the same functionality as an existing route, then update the dashboard to display both routes in a clean comparison table format.
/main.tsx
) to understand current routing setup/backend/routes/views/_views.routes.ts
/backend/routes/glimpse/
/backend/routes/glimpse/_glimpse.routes.ts
/:id
(not /glimpse/:id
since mounting handles prefix)/backend/routes/glimpse/README.md
/main.tsx
/backend/routes/views/dashboard.tsx
/views/glimpse/{:id}
as clickable link/glimpse/{:id}
as clickable linkimport { Hono } from "npm:hono@3.12.12";
import { glimpseHandler } from "../../controllers/glimpse.controller.ts";
const app = new Hono();
app.get("/:id", glimpseHandler);
export default app;
import glimpseRoutes from "./backend/routes/glimpse/_glimpse.routes.ts";
app.use("/glimpse/*", authCheck);
app.route("/glimpse", glimpseRoutes);
This approach creates a clean duplicate route with an intuitive dashboard display that demonstrates both URL formats side-by-side.
Townie, stop here! Before proceeding to additional steps, confirm that this step is working correctly.
If everything is working as intended: conclude this step with these messages:
Follow the instructions in
/_townie/07-save.md
to add the/tasks/url
endpoint