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:
- How the route is currently defined (e.g.,
app.get("/feature/:id", handler)
) - What authentication middleware is applied
- How routes are mounted in main.tsx
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:
- Route path is
/:id
not/feature/:id
(mounting handles the prefix) - Import the same controller used by the original route
- Follow the same naming convention (
_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:
- Import route modules (not controllers)
- Apply authentication middleware
- Mount routes after middleware
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;
- Main router → Route modules → Controllers → Services
- Never import controllers directly in main router
- Each route module handles one logical grouping
- Apply same middleware to both route paths
- Ensure identical security behavior
- Document authentication requirements
- Reuse existing controllers and services
- Don't duplicate business logic
- Share the same handler implementation
- Both routes return identical responses
- Authentication works the same on both routes
- No 404 errors on either route
- Route parameters are correctly extracted
- Error handling is consistent
- Documentation is updated
- Check route mounting path vs route definition path
- Verify authentication middleware is applied before route mounting
- Ensure route module exports default app
- Verify both routes use the same controller
- Check that authentication middleware is applied to both
- Confirm route parameters are extracted identically
- Never import controllers in main router
- Always go through route modules
- Maintain consistent file naming conventions
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.
- Check main router (
/main.tsx
) to understand current routing setup - Identify the existing route in
/backend/routes/views/_views.routes.ts
- Locate the controller that handles the route functionality
- Note the authentication middleware applied to the existing route
- Create directory structure:
/backend/routes/glimpse/
- Create route file:
/backend/routes/glimpse/_glimpse.routes.ts
- Import Hono and the existing controller
- Define route as
/:id
(not/glimpse/:id
since mounting handles prefix) - Export the Hono app as default
- Create documentation:
/backend/routes/glimpse/README.md
- Document the route purpose, authentication, parameters, and response
- Note that it's equivalent to the original route
- Add import for the new route module in
/main.tsx
- Add authentication middleware for the new route path (same as original)
- Mount the route after middleware setup
- Maintain proper order: imports → middleware → route mounting
- Locate the dashboard file at
/backend/routes/views/dashboard.tsx
- Find the section that displays glimpse links (look for demo pages display)
- Replace the existing link display with a three-column table:
- Column 1: "Name" (30% width) - shows page title with emoji
- Column 2: "Original Route" (35% width) - shows
/views/glimpse/{:id}
as clickable link - Column 3: "New Shortcut Route" (35% width) - shows
/glimpse/{:id}
as clickable link
- Style the table with:
- Clean borders and spacing
- Header row with background color
- Different link colors (blue for original, green for new)
- Monospace font for route paths
- Proper vertical alignment
import { 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:
import glimpseRoutes from "./backend/routes/glimpse/_glimpse.routes.ts";
- Auth:
app.use("/glimpse/*", authCheck);
- Mount:
app.route("/glimpse", glimpseRoutes);
- Three columns with clear headers
- Page name in first column (no repetition)
- Both route formats as clickable links
- Consistent styling and spacing
- Color coding for route types
- Test both routes return identical responses (login page if not authenticated)
- Verify table display shows all demo pages with both route formats
- Confirm links work and open in new tabs
- Check responsive layout and readability
- No code duplication - reuse existing controller
- Consistent authentication - same middleware for both routes
- Clean architecture - maintain separation between router, routes, and controllers
- User-friendly display - clear comparison table without repetition
- Functional testing - both routes must behave identically
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:
- Briefly summarize the work that was just completed and why it's important
- Provide the URL to the dashboard that you just updated so the user can see that the new routes are live. Tell the user that they can copy and paste that URL into a new tab to see the links.
Follow the instructions in
/_townie/07-save.md
to add the/tasks/url
endpoint