From 669ef25f8703382bbe9de902ab849e4a5d585ae2 Mon Sep 17 00:00:00 2001 From: Aaron Jordan Date: Fri, 10 Oct 2025 13:57:21 -0700 Subject: [PATCH 1/6] Add consistent alias functions for standard HTTP verb paths --- packages/fetch-router/CHANGELOG.md | 1 + packages/fetch-router/src/index.ts | 11 +++++++ .../fetch-router/src/lib/route-helpers.ts | 29 +++++++++++++++++++ 3 files changed, 41 insertions(+) create mode 100644 packages/fetch-router/src/lib/route-helpers.ts diff --git a/packages/fetch-router/CHANGELOG.md b/packages/fetch-router/CHANGELOG.md index bfabc1eddb4..a44fa7cf8ff 100644 --- a/packages/fetch-router/CHANGELOG.md +++ b/packages/fetch-router/CHANGELOG.md @@ -32,6 +32,7 @@ This is the changelog for [`fetch-router`](https://github.com/remix-run/remix/tr - Export `InferRouteHandler` and `InferRequestHandler` types - Re-export `FormDataParseError`, `FileUpload`, and `FileUploadHandler` from `@remix-run/form-data-parser` - Fix an issue where per-route middleware was not being applied to a route handler nested inside a route map with its own middleware +- Adds functional aliases for routes that respond to a single HTTP verb ## v0.5.0 (2025-10-05) diff --git a/packages/fetch-router/src/index.ts b/packages/fetch-router/src/index.ts index 387a2a152e0..86fa68aa6f2 100644 --- a/packages/fetch-router/src/index.ts +++ b/packages/fetch-router/src/index.ts @@ -38,6 +38,17 @@ export type { export type { RouteHandlers, InferRouteHandler, RouteHandler } from './lib/route-handlers.ts' +export { + createDelete, + createDelete as delete, // shorthand + createGet, + createGet as get, // shorthand + createPost, + createPost as post, // shorthand + createPut, + createPut as put // shorthand +} from './lib/route-helpers.ts' + export { Route, createRoutes, diff --git a/packages/fetch-router/src/lib/route-helpers.ts b/packages/fetch-router/src/lib/route-helpers.ts new file mode 100644 index 00000000000..5967754b0d2 --- /dev/null +++ b/packages/fetch-router/src/lib/route-helpers.ts @@ -0,0 +1,29 @@ +import type { RoutePattern } from '@remix-run/route-pattern' + +/** + * Shorthand for a DELETE route. + */ +export function createDelete

(pattern: P | RoutePattern

) { + return { method: 'DELETE', pattern }; +} + +/** + * Shorthand for a GET route. + */ +export function createGet

(pattern: P | RoutePattern

) { + return { method: 'GET', pattern }; +} + +/** + * Shorthand for a POST route. + */ +export function createPost

(pattern: P | RoutePattern

) { + return { method: 'POST', pattern }; +} + +/** + * Shorthand for a PUT route. + */ +export function createPut

(pattern: P | RoutePattern

) { + return { method: 'PUT', pattern }; +} From 439e925d976c44ea483191b123e6949e31006c4b Mon Sep 17 00:00:00 2001 From: Aaron Jordan Date: Fri, 10 Oct 2025 15:06:41 -0700 Subject: [PATCH 2/6] Remove misplaced changelog entry --- packages/fetch-router/CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/fetch-router/CHANGELOG.md b/packages/fetch-router/CHANGELOG.md index a44fa7cf8ff..bfabc1eddb4 100644 --- a/packages/fetch-router/CHANGELOG.md +++ b/packages/fetch-router/CHANGELOG.md @@ -32,7 +32,6 @@ This is the changelog for [`fetch-router`](https://github.com/remix-run/remix/tr - Export `InferRouteHandler` and `InferRequestHandler` types - Re-export `FormDataParseError`, `FileUpload`, and `FileUploadHandler` from `@remix-run/form-data-parser` - Fix an issue where per-route middleware was not being applied to a route handler nested inside a route map with its own middleware -- Adds functional aliases for routes that respond to a single HTTP verb ## v0.5.0 (2025-10-05) From 2309d83f3f2a08811f690a594d3abb1e32d25250 Mon Sep 17 00:00:00 2001 From: Aaron Jordan Date: Sat, 25 Oct 2025 13:43:04 -0700 Subject: [PATCH 3/6] add narrowing type helper for RouteDef and tests for route-helpers --- packages/fetch-router/CHANGELOG.md | 1 + packages/fetch-router/src/index.ts | 10 +- .../src/lib/route-helpers.test.ts | 131 ++++++++++++++++++ .../fetch-router/src/lib/route-helpers.ts | 45 ++++-- 4 files changed, 177 insertions(+), 10 deletions(-) create mode 100644 packages/fetch-router/src/lib/route-helpers.test.ts diff --git a/packages/fetch-router/CHANGELOG.md b/packages/fetch-router/CHANGELOG.md index e48430aa2fc..24347f1214a 100644 --- a/packages/fetch-router/CHANGELOG.md +++ b/packages/fetch-router/CHANGELOG.md @@ -50,6 +50,7 @@ This is the changelog for [`fetch-router`](https://github.com/remix-run/remix/tr - Add support for `request.signal` abort, which now short-circuits the middleware chain. `router.fetch()` will now throw `DOMException` with `error.name === 'AbortError'` when a request is aborted - Fix an issue where `Router`'s `fetch` wasn't spec-compliant - Provide empty `context.formData` to `POST`/`PUT`/etc handlers when `parseFormData: false` +- Add functional aliases for routes that respond to a single HTTP verb ## v0.6.0 (2025-10-10) diff --git a/packages/fetch-router/src/index.ts b/packages/fetch-router/src/index.ts index fa21406a342..f2dd738df49 100644 --- a/packages/fetch-router/src/index.ts +++ b/packages/fetch-router/src/index.ts @@ -42,13 +42,19 @@ export type { RouteHandlers, InferRouteHandler, RouteHandler } from './lib/route export { createDelete, - createDelete as delete, // shorthand + createDelete as destroy, // shorthand createGet, createGet as get, // shorthand + createHead, + createHead as head, // shorthand + createOptions, + createOptions as options, // shorthand + createPatch, + createPatch as patch, // shorthand createPost, createPost as post, // shorthand createPut, - createPut as put // shorthand + createPut as put, // shorthand } from './lib/route-helpers.ts' export { diff --git a/packages/fetch-router/src/lib/route-helpers.test.ts b/packages/fetch-router/src/lib/route-helpers.test.ts new file mode 100644 index 00000000000..62f73ad8b95 --- /dev/null +++ b/packages/fetch-router/src/lib/route-helpers.test.ts @@ -0,0 +1,131 @@ +import * as assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import type { Assert, IsEqual } from './type-utils.ts' +import { Route, createRoutes as route } from './route-map.ts' +import { destroy, get, head, options, patch, post, put } from '../index.ts' +import { RoutePattern } from '@remix-run/route-pattern' + +describe('route helpers composition', () => { + it('composes route helpers in a route map', () => { + let routes = route({ + home: '/', + posts: { + index: get('/posts'), + create: post('/posts'), + show: get('/posts/:id'), + update: put('/posts/:id'), + patch: patch('/posts/:id'), + destroy: destroy('/posts/:id'), + }, + api: { + health: head('/api/health'), + options: options('/api/settings'), + }, + }) + + assert.deepEqual(routes.home, new Route('ANY', '/')) + assert.deepEqual(routes.posts.index, new Route('GET', '/posts')) + assert.deepEqual(routes.posts.create, new Route('POST', '/posts')) + assert.deepEqual(routes.posts.show, new Route('GET', '/posts/:id')) + assert.deepEqual(routes.posts.update, new Route('PUT', '/posts/:id')) + assert.deepEqual(routes.posts.patch, new Route('PATCH', '/posts/:id')) + assert.deepEqual(routes.posts.destroy, new Route('DELETE', '/posts/:id')) + assert.deepEqual(routes.api.health, new Route('HEAD', '/api/health')) + assert.deepEqual(routes.api.options, new Route('OPTIONS', '/api/settings')) + }) + + it('composes route helpers with base paths', () => { + let apiRoutes = route('api/v1', { + users: { + index: get('/'), + create: post('/'), + show: get('/:id'), + update: put('/:id'), + destroy: destroy('/:id'), + }, + }) + + let routes = route({ + home: '/', + api: apiRoutes, + }) + + assert.deepEqual(routes.api.users.index, new Route('GET', '/api/v1')) + assert.deepEqual(routes.api.users.create, new Route('POST', '/api/v1')) + assert.deepEqual(routes.api.users.show, new Route('GET', '/api/v1/:id')) + assert.deepEqual(routes.api.users.update, new Route('PUT', '/api/v1/:id')) + assert.deepEqual(routes.api.users.destroy, new Route('DELETE', '/api/v1/:id')) + }) + + it('mixes helper methods with string patterns', () => { + let routes = route({ + home: '/', + about: '/about', + contact: get('/contact'), + login: post('/auth/login'), + logout: destroy('/auth/logout'), + profile: { + show: '/profile', + edit: get('/profile/edit'), + update: patch('/profile'), + }, + }) + + assert.deepEqual(routes.home, new Route('ANY', '/')) + assert.deepEqual(routes.about, new Route('ANY', '/about')) + assert.deepEqual(routes.contact, new Route('GET', '/contact')) + assert.deepEqual(routes.login, new Route('POST', '/auth/login')) + assert.deepEqual(routes.logout, new Route('DELETE', '/auth/logout')) + assert.deepEqual(routes.profile.show, new Route('ANY', '/profile')) + assert.deepEqual(routes.profile.edit, new Route('GET', '/profile/edit')) + assert.deepEqual(routes.profile.update, new Route('PATCH', '/profile')) + }) + + it('uses helper methods with complex patterns', () => { + let routes = route({ + api: { + posts: get('/api/posts(/:lang)'), + createPost: post('/api/posts'), + updatePost: put('/api/posts/:id'), + deletePost: destroy('/api/posts/:id'), + }, + healthCheck: head('/health'), + }) + + assert.deepEqual(routes.api.posts, new Route('GET', '/api/posts(/:lang)')) + assert.deepEqual(routes.api.createPost, new Route('POST', '/api/posts')) + assert.deepEqual(routes.api.updatePost, new Route('PUT', '/api/posts/:id')) + assert.deepEqual(routes.api.deletePost, new Route('DELETE', '/api/posts/:id')) + assert.deepEqual(routes.healthCheck, new Route('HEAD', '/health')) + }) +}) + +let composedRoutes = route({ + home: '/', + ...route('posts', { + posts: get('/'), + createPost: post('/'), + showPost: get(':id'), + updatePost: put(':id'), + deletePost: destroy(':id'), + }), + api: { + health: head('/api/health'), + options: options('/api/settings'), + }, + patch: patch(new RoutePattern('/patch')), + put: put(new RoutePattern('/misc/put')), +}) + +type Tests = [ + Assert>>, + Assert>>, + Assert>>, + Assert>>, + Assert>>, + Assert>>, + Assert>>, + Assert>>, + Assert>>, +] diff --git a/packages/fetch-router/src/lib/route-helpers.ts b/packages/fetch-router/src/lib/route-helpers.ts index 5967754b0d2..73cba53f5ec 100644 --- a/packages/fetch-router/src/lib/route-helpers.ts +++ b/packages/fetch-router/src/lib/route-helpers.ts @@ -1,29 +1,58 @@ import type { RoutePattern } from '@remix-run/route-pattern' +import type { RequestMethod } from './request-methods' + /** * Shorthand for a DELETE route. */ -export function createDelete

(pattern: P | RoutePattern

) { - return { method: 'DELETE', pattern }; +export function createDelete(pattern: T) { + return { method: 'DELETE', pattern: pattern } as BuildRouteDef<'DELETE', T> } /** * Shorthand for a GET route. */ -export function createGet

(pattern: P | RoutePattern

) { - return { method: 'GET', pattern }; +export function createGet(pattern: T) { + return { method: 'GET', pattern: pattern } as BuildRouteDef<'GET', T> +} + +/** + * Shorthand for a HEAD route. + */ +export function createHead(pattern: T) { + return { method: 'HEAD', pattern: pattern } as BuildRouteDef<'HEAD', T> +} + +/** + * Shorthand for a OPTIONS route. + */ +export function createOptions(pattern: T) { + return { method: 'OPTIONS', pattern: pattern } as BuildRouteDef<'OPTIONS', T> +} + +/** + * Shorthand for a PATCH route. + */ +export function createPatch(pattern: T) { + return { method: 'PATCH', pattern: pattern } as BuildRouteDef<'PATCH', T> } /** * Shorthand for a POST route. */ -export function createPost

(pattern: P | RoutePattern

) { - return { method: 'POST', pattern }; +export function createPost(pattern: T) { + return { method: 'POST', pattern: pattern } as BuildRouteDef<'POST', T> } /** * Shorthand for a PUT route. */ -export function createPut

(pattern: P | RoutePattern

) { - return { method: 'PUT', pattern }; +export function createPut(pattern: T) { + return { method: 'PUT', pattern: pattern } as BuildRouteDef<'PUT', T> } + +// prettier-ignore +type BuildRouteDef = + T extends string ? {method: M, pattern: T} : + T extends RoutePattern ? {method: M, pattern: RoutePattern

} : + never; From cc3c1454815c607367e4389d84c9532ca0ebc2f7 Mon Sep 17 00:00:00 2001 From: Aaron Jordan Date: Sat, 25 Oct 2025 13:52:13 -0700 Subject: [PATCH 4/6] rename createDelete for consistency with shorthand --- packages/fetch-router/src/lib/route-helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/fetch-router/src/lib/route-helpers.ts b/packages/fetch-router/src/lib/route-helpers.ts index 73cba53f5ec..8c2354e7e7d 100644 --- a/packages/fetch-router/src/lib/route-helpers.ts +++ b/packages/fetch-router/src/lib/route-helpers.ts @@ -5,7 +5,7 @@ import type { RequestMethod } from './request-methods' /** * Shorthand for a DELETE route. */ -export function createDelete(pattern: T) { +export function createDestroy(pattern: T) { return { method: 'DELETE', pattern: pattern } as BuildRouteDef<'DELETE', T> } From 05c7650f0420b5a087687bc383a726596f06551c Mon Sep 17 00:00:00 2001 From: Aaron Jordan Date: Sat, 25 Oct 2025 14:05:28 -0700 Subject: [PATCH 5/6] correct index export --- packages/fetch-router/src/index.ts | 4 ++-- packages/fetch-router/src/lib/route-helpers.ts | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/fetch-router/src/index.ts b/packages/fetch-router/src/index.ts index f2dd738df49..5a9977391b3 100644 --- a/packages/fetch-router/src/index.ts +++ b/packages/fetch-router/src/index.ts @@ -41,8 +41,8 @@ export type { export type { RouteHandlers, InferRouteHandler, RouteHandler } from './lib/route-handlers.ts' export { - createDelete, - createDelete as destroy, // shorthand + createDestroy, + createDestroy as destroy, // shorthand createGet, createGet as get, // shorthand createHead, diff --git a/packages/fetch-router/src/lib/route-helpers.ts b/packages/fetch-router/src/lib/route-helpers.ts index 8c2354e7e7d..7114b1ab207 100644 --- a/packages/fetch-router/src/lib/route-helpers.ts +++ b/packages/fetch-router/src/lib/route-helpers.ts @@ -6,53 +6,53 @@ import type { RequestMethod } from './request-methods' * Shorthand for a DELETE route. */ export function createDestroy(pattern: T) { - return { method: 'DELETE', pattern: pattern } as BuildRouteDef<'DELETE', T> + return { method: 'DELETE', pattern: pattern } as BuildRouteObject<'DELETE', T> } /** * Shorthand for a GET route. */ export function createGet(pattern: T) { - return { method: 'GET', pattern: pattern } as BuildRouteDef<'GET', T> + return { method: 'GET', pattern: pattern } as BuildRouteObject<'GET', T> } /** * Shorthand for a HEAD route. */ export function createHead(pattern: T) { - return { method: 'HEAD', pattern: pattern } as BuildRouteDef<'HEAD', T> + return { method: 'HEAD', pattern: pattern } as BuildRouteObject<'HEAD', T> } /** * Shorthand for a OPTIONS route. */ export function createOptions(pattern: T) { - return { method: 'OPTIONS', pattern: pattern } as BuildRouteDef<'OPTIONS', T> + return { method: 'OPTIONS', pattern: pattern } as BuildRouteObject<'OPTIONS', T> } /** * Shorthand for a PATCH route. */ export function createPatch(pattern: T) { - return { method: 'PATCH', pattern: pattern } as BuildRouteDef<'PATCH', T> + return { method: 'PATCH', pattern: pattern } as BuildRouteObject<'PATCH', T> } /** * Shorthand for a POST route. */ export function createPost(pattern: T) { - return { method: 'POST', pattern: pattern } as BuildRouteDef<'POST', T> + return { method: 'POST', pattern: pattern } as BuildRouteObject<'POST', T> } /** * Shorthand for a PUT route. */ export function createPut(pattern: T) { - return { method: 'PUT', pattern: pattern } as BuildRouteDef<'PUT', T> + return { method: 'PUT', pattern: pattern } as BuildRouteObject<'PUT', T> } // prettier-ignore -type BuildRouteDef = +type BuildRouteObject = T extends string ? {method: M, pattern: T} : T extends RoutePattern ? {method: M, pattern: RoutePattern

} : never; From de7ebf135711f3461e1986987b964bafd13b79af Mon Sep 17 00:00:00 2001 From: Aaron Jordan Date: Sat, 25 Oct 2025 14:07:09 -0700 Subject: [PATCH 6/6] rename route def object type --- packages/fetch-router/src/lib/route-helpers.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/fetch-router/src/lib/route-helpers.ts b/packages/fetch-router/src/lib/route-helpers.ts index 7114b1ab207..bee7168bbe0 100644 --- a/packages/fetch-router/src/lib/route-helpers.ts +++ b/packages/fetch-router/src/lib/route-helpers.ts @@ -6,53 +6,53 @@ import type { RequestMethod } from './request-methods' * Shorthand for a DELETE route. */ export function createDestroy(pattern: T) { - return { method: 'DELETE', pattern: pattern } as BuildRouteObject<'DELETE', T> + return { method: 'DELETE', pattern: pattern } as RouteDefObject<'DELETE', T> } /** * Shorthand for a GET route. */ export function createGet(pattern: T) { - return { method: 'GET', pattern: pattern } as BuildRouteObject<'GET', T> + return { method: 'GET', pattern: pattern } as RouteDefObject<'GET', T> } /** * Shorthand for a HEAD route. */ export function createHead(pattern: T) { - return { method: 'HEAD', pattern: pattern } as BuildRouteObject<'HEAD', T> + return { method: 'HEAD', pattern: pattern } as RouteDefObject<'HEAD', T> } /** * Shorthand for a OPTIONS route. */ export function createOptions(pattern: T) { - return { method: 'OPTIONS', pattern: pattern } as BuildRouteObject<'OPTIONS', T> + return { method: 'OPTIONS', pattern: pattern } as RouteDefObject<'OPTIONS', T> } /** * Shorthand for a PATCH route. */ export function createPatch(pattern: T) { - return { method: 'PATCH', pattern: pattern } as BuildRouteObject<'PATCH', T> + return { method: 'PATCH', pattern: pattern } as RouteDefObject<'PATCH', T> } /** * Shorthand for a POST route. */ export function createPost(pattern: T) { - return { method: 'POST', pattern: pattern } as BuildRouteObject<'POST', T> + return { method: 'POST', pattern: pattern } as RouteDefObject<'POST', T> } /** * Shorthand for a PUT route. */ export function createPut(pattern: T) { - return { method: 'PUT', pattern: pattern } as BuildRouteObject<'PUT', T> + return { method: 'PUT', pattern: pattern } as RouteDefObject<'PUT', T> } // prettier-ignore -type BuildRouteObject = +type RouteDefObject = T extends string ? {method: M, pattern: T} : T extends RoutePattern ? {method: M, pattern: RoutePattern

} : never;