Skip to content

feat: add removeRoute#1331

Merged
pi0 merged 4 commits intomainfrom
feat/remove-route
Mar 19, 2026
Merged

feat: add removeRoute#1331
pi0 merged 4 commits intomainfrom
feat/remove-route

Conversation

@productdevbook
Copy link
Copy Markdown
Member

@productdevbook productdevbook commented Mar 14, 2026

Summary

Closes #1159

Adds removeRoute utility to remove registered route handlers.

import { removeRoute } from "h3";

const app = new H3();

app.get("/temp", () => "hello");
// GET /temp → 200 "hello"

removeRoute(app, "GET", "/temp");
// GET /temp → 404

Implementation

  • Tree-shakeable standalone utility (not an H3 class method)
  • Uses rou3's removeRoute() to remove from the router context
  • Also removes from the internal ~routes array for consistency
  • Method-specific: removeRoute(app, "GET", "/path") only removes GET, leaves POST etc.
  • Empty method only targets methodless entries — does not accidentally remove all methods on the same path

Test plan

  • Removes a registered route (returns 404 after removal)
  • Only removes the specified method (POST still works after removing GET)
  • Empty method does not remove method-specific routes
  • All tests pass in both web and node modes via describeMatrix

Summary by CodeRabbit

Release Notes

  • New Features
    • Route removal functionality is now available, enabling dynamic cleanup and management of registered handlers in your application.
    • Method-specific removal is fully supported, allowing you to precisely unregister individual HTTP methods while keeping others intact on the same route path.

Add off(method, route) method to remove registered route handlers.
Uses rou3's removeRoute under the hood and also cleans up the internal
~routes array.

app.get("/temp", () => "hello");
app.off("GET", "/temp"); // route removed, returns 404

Closes #1159

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@productdevbook productdevbook requested a review from pi0 as a code owner March 14, 2026 21:41
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 14, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8e365451-1a8c-4bbb-8dcd-a7e34891451a

📥 Commits

Reviewing files that changed from the base of the PR and between 9c6eb9f and 66cbc6c.

📒 Files selected for processing (4)
  • src/index.ts
  • src/utils/route.ts
  • test/router.test.ts
  • test/unit/package.test.ts

📝 Walkthrough

Walkthrough

This pull request introduces a new removeRoute utility function that enables removal of registered routes from an H3 application by specifying HTTP method and path. The implementation normalizes inputs, delegates to the underlying rou3 removal routine, and cleans up internal route tracking. Comprehensive tests validate method-specific removal, path handling, and behavior with empty method strings.

Changes

Cohort / File(s) Summary
Route removal implementation
src/utils/route.ts
Adds removeRoute(app, method, route) function that normalizes method and route, invokes rou3's removal routine, and removes matching entries from app's internal route tracking.
API exports
src/index.ts
Re-exports removeRoute from ./utils/route.ts alongside existing type RouteDefinition and defineRoute; updates section comment from "Route" to "Routing".
Test coverage
test/router.test.ts
Adds comprehensive test suite for removeRoute including basic removal, method-specific removal with mixed methods on same path, and behavior with empty method strings.
Package snapshot
test/unit/package.test.ts
Updates exported API snapshot to include new removeRoute entry.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

  • feat: add removeRoute #1331: Implements identical removeRoute utility with same logic for removing routes by method and path, updates the same exports and test infrastructure.

Poem

🐰 A path to unbuild, a route to delete,
With method so matched, the cleanup's complete!
Through rou3 we venture, the old trails we sweep,
Fresh routes for fresh starts—hop deep! 🌿

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed The PR fully addresses issue #1159 by implementing route removal functionality with method-specific removal (GET vs POST) and the removeRoute utility for tree-shaking optimization.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing the removeRoute utility and updating exports; no unrelated modifications are present.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Title check ✅ Passed The title clearly and accurately describes the main change: adding a new removeRoute function to the public API.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/remove-route
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
test/router.test.ts (1)

225-252: LGTM!

The tests cover the core functionality well: basic route removal and method-specific removal. The test structure follows the established patterns with describeMatrix.

Consider adding edge case coverage for completeness:

  • Removing a non-existent route (no-op behavior)
  • Using lowercase method strings (e.g., off("get", "/path"))
  • Verifying chaining works (e.g., t.app.off(...).off(...))
💡 Example additional test
it("handles lowercase method and non-existent routes gracefully", async () => {
  t.app.get("/lower", () => "ok");
  
  // Lowercase method should work
  t.app.off("get", "/lower");
  const res = await t.fetch("/lower");
  expect(res.status).toBe(404);
  
  // Removing non-existent route should not throw
  expect(() => t.app.off("GET", "/does-not-exist")).not.toThrow();
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/router.test.ts` around lines 225 - 252, Add tests covering edge cases
for the router off behavior: verify that calling t.app.off with a lowercase
method string (e.g., "get") removes the route the same as uppercase, that
removing a non-existent route is a no-op and does not throw, and that off
returns the app (or is chainable) so chaining t.app.off(...).off(...) works;
update the test file's "off (remove route)" suite to add one spec that registers
a route with t.app.get, calls t.app.off("get", "/path") and asserts 404 on
fetch, asserts that calling t.app.off("GET", "/does-not-exist") does not throw,
and asserts chaining by checking the return value of t.app.off is the app (or by
chaining two off calls and verifying expected outcome).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/h3.ts`:
- Around line 153-164: The off method currently calls removeRoute(this["~rou3"],
_method, route) which deletes all matching entries from rou3 but then uses
findIndex + splice on this["~routes"], removing only the first match and leaving
stale entries; update off (the function handling route removal) to filter
this["~routes"] and remove every entry where r.route === route and (no _method
or r.method === _method) so the internal ~routes array stays in sync with
removeRoute/rou3.

---

Nitpick comments:
In `@test/router.test.ts`:
- Around line 225-252: Add tests covering edge cases for the router off
behavior: verify that calling t.app.off with a lowercase method string (e.g.,
"get") removes the route the same as uppercase, that removing a non-existent
route is a no-op and does not throw, and that off returns the app (or is
chainable) so chaining t.app.off(...).off(...) works; update the test file's
"off (remove route)" suite to add one spec that registers a route with
t.app.get, calls t.app.off("get", "/path") and asserts 404 on fetch, asserts
that calling t.app.off("GET", "/does-not-exist") does not throw, and asserts
chaining by checking the return value of t.app.off is the app (or by chaining
two off calls and verifying expected outcome).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 563496ba-9d27-4506-82ad-637179584dae

📥 Commits

Reviewing files that changed from the base of the PR and between 5da2989 and 3623924.

📒 Files selected for processing (3)
  • src/h3.ts
  • src/types/h3.ts
  • test/router.test.ts

src/h3.ts Outdated
Comment on lines +153 to +164
off(method: HTTPMethod | Lowercase<HTTPMethod> | "", route: string): this {
const _method = (method || "").toUpperCase();
route = new URL(route, "http://_").pathname;
removeRoute(this["~rou3"], _method, route);
const idx = this["~routes"].findIndex(
(r) => r.route === route && (!_method || r.method === _method),
);
if (idx !== -1) {
this["~routes"].splice(idx, 1);
}
return this;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

rou3 removeRoute behavior with duplicate routes

💡 Result:

In h3js/rou3 (JS), duplicate routes (same method + same path pattern) are allowed:

  • addRoute() stores routes in an array per node+method and pushes each new entry, so duplicates accumulate rather than overwrite. [1]
  • findRoute() returns only the first match for that method/path (it returns match[0]), so with duplicates you’ll get the earliest-added handler/data. [2]
  • findAllRoutes() returns all matches, including duplicates (it spreads/pushes the whole per-method array into results). [3]
  • removeRoute() deletes node.methods[method] entirely (not a single array element), so it removes all duplicates registered at that exact node+method in one call. There’s no API to remove “just one” of the duplicates. [4]

Sources:
[1] add.ts route storage (node.methods![method] ??= []; ... push(...)) (github.com)
[2] find.ts returns first match (return staticMatch[0], ...?.[0]) (raw.githubusercontent.com)
[3] find-all.ts pushes all matches (matches.push(...match)) (raw.githubusercontent.com)
[4] remove.ts deletes method entry (delete node.methods[method]) (raw.githubusercontent.com)

Citations:


Remove all matching route entries, not just the first.

The removeRoute() call removes all matching entries from rou3, but findIndex + splice removes only the first matching entry from ~routes. When a route is registered multiple times with the same method and path, this leaves stale entries in the internal ~routes array that become out of sync with rou3.

Use filter to remove all matching entries consistently:

Proposed fix
     off(method: HTTPMethod | Lowercase<HTTPMethod> | "", route: string): this {
       const _method = (method || "").toUpperCase();
       route = new URL(route, "http://_").pathname;
       removeRoute(this["~rou3"], _method, route);
-      const idx = this["~routes"].findIndex(
-        (r) => r.route === route && (!_method || r.method === _method),
-      );
-      if (idx !== -1) {
-        this["~routes"].splice(idx, 1);
-      }
+      this["~routes"] = this["~routes"].filter(
+        (r) => !(r.route === route && (!_method || r.method === _method)),
+      );
       return this;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/h3.ts` around lines 153 - 164, The off method currently calls
removeRoute(this["~rou3"], _method, route) which deletes all matching entries
from rou3 but then uses findIndex + splice on this["~routes"], removing only the
first match and leaving stale entries; update off (the function handling route
removal) to filter this["~routes"] and remove every entry where r.route ===
route and (no _method or r.method === _method) so the internal ~routes array
stays in sync with removeRoute/rou3.

@pi0
Copy link
Copy Markdown
Member

pi0 commented Mar 14, 2026

We ca introduce a removeRoute utility and avoid adding to the bundle size of H3

Per maintainer review: avoid adding methods to H3 class to keep
bundle size small. removeRoute is now a tree-shakeable standalone
utility function instead of app.off().

Usage:
  import { removeRoute } from "h3";
  removeRoute(app, "GET", "/temp");

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Mar 15, 2026

Open in StackBlitz

npm i https://pkg.pr.new/h3@1331

commit: 66cbc6c

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/utils/route.remove.ts`:
- Around line 25-27: The findIndex predicate treats empty-string _method as
falsy, so (!_method || r.method === _method) matches all routes and can remove
e.g. GET/POST entries from app["~routes"]; update the predicate to only treat
null/undefined as the wildcard by replacing (!_method || r.method === _method)
with (_method == null || r.method === _method) in the findIndex call
(referencing idx, app["~routes"], r.route, r.method and _method) so only truly
absent methods match the methodless/all-method entry.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a59f34fa-16c2-49da-819f-705948fdfe21

📥 Commits

Reviewing files that changed from the base of the PR and between 3623924 and 9c6eb9f.

📒 Files selected for processing (4)
  • src/index.ts
  • src/utils/route.remove.ts
  • test/router.test.ts
  • test/unit/package.test.ts

@pi0 pi0 changed the title feat: add app.off() to remove route handlers feat: add removeRoute Mar 15, 2026
@pi0 pi0 merged commit fe9477f into main Mar 19, 2026
6 of 7 checks passed
@pi0 pi0 deleted the feat/remove-route branch March 19, 2026 19:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

How to remove route…

2 participants