glimpse2-runbook
Val Town is a collaborative website to build and scale JavaScript apps.
Deploy APIs, crons, & store data – all from the browser, and deployed in milliseconds.
Viewing readonly version of main branch: v10View latest version
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.