Skip to content

Commit 610428d

Browse files
authored
fix(fastify): fix when nested routers are used with pathPrefix (#632)
1 parent 24dec20 commit 610428d

3 files changed

Lines changed: 71 additions & 33 deletions

File tree

.changeset/good-apes-study.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@ts-rest/fastify': patch
3+
---
4+
5+
Fix when `pathPrefix` is used multiple times in nested contracts

libs/ts-rest/fastify/src/lib/ts-rest-fastify.spec.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,54 @@ describe('ts-rest-fastify', () => {
358358
expect(response.body).toEqual({ id: '10' });
359359
});
360360

361+
it('prefixed contract should work with fastify sub-routers', async () => {
362+
const postsContractNested = c.router(
363+
{
364+
getPost: {
365+
path: '/:postId',
366+
method: 'GET',
367+
responses: { 200: c.type<{ id: string }>() },
368+
},
369+
},
370+
{ pathPrefix: '/posts' },
371+
);
372+
373+
const mainContract = c.router(
374+
{
375+
health: {
376+
method: 'GET',
377+
path: '/health',
378+
responses: { 200: c.type<{ message: string }>() },
379+
},
380+
posts: postsContractNested,
381+
},
382+
{ pathPrefix: '/v1' },
383+
);
384+
385+
const postsRouter = s.router(mainContract.posts, {
386+
getPost: async ({ params }) => {
387+
return { status: 200, body: { id: params.postId } };
388+
},
389+
});
390+
391+
const router = s.router(mainContract, {
392+
health: async () => {
393+
return { status: 200, body: { message: 'ok' } };
394+
},
395+
posts: postsRouter,
396+
});
397+
398+
const app = fastify();
399+
s.registerRouter(mainContract, router, app);
400+
401+
await app.ready();
402+
403+
await supertest(app.server).get('/v1/posts/10').expect(200, { id: '10' });
404+
await supertest(app.server)
405+
.get('/v1/health')
406+
.expect(200, { message: 'ok' });
407+
});
408+
361409
it('should handle non-json response types from contract', async () => {
362410
const c = initContract();
363411

libs/ts-rest/fastify/src/lib/ts-rest-fastify.ts

Lines changed: 18 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -50,17 +50,12 @@ type AppRouteImplementation<T extends AppRoute> = (
5050

5151
type RecursiveRouterObj<T extends AppRouter> = {
5252
[TKey in keyof T]: T[TKey] extends AppRouter
53-
? InitialisedRouter<T[TKey]> | RecursiveRouterObj<T[TKey]>
53+
? RecursiveRouterObj<T[TKey]>
5454
: T[TKey] extends AppRoute
5555
? AppRouteImplementation<T[TKey]>
5656
: never;
5757
};
5858

59-
type InitialisedRouter<TContract extends AppRouter> = {
60-
contract: TContract;
61-
routes: RecursiveRouterObj<TContract>;
62-
};
63-
6459
type RegisterRouterOptions = {
6560
logInitialization?: boolean;
6661
jsonQuery?: boolean;
@@ -122,20 +117,22 @@ const validateRequest = (
122117
};
123118
};
124119

120+
const RouterEmbeddedContract = Symbol('RouterEmbeddedContract');
121+
125122
export const initServer = () => ({
126123
router: <TContract extends AppRouter>(
127124
contract: TContract,
128125
routes: RecursiveRouterObj<TContract>,
129-
): InitialisedRouter<TContract> => ({
130-
contract,
131-
routes,
126+
): RecursiveRouterObj<TContract> => ({
127+
...routes,
128+
[RouterEmbeddedContract]: contract,
132129
}),
133130
route: <TAppRoute extends AppRoute>(
134131
route: TAppRoute,
135132
implementation: AppRouteImplementation<TAppRoute>,
136133
) => implementation,
137134
registerRouter: <
138-
T extends InitialisedRouter<TContract>,
135+
T extends RecursiveRouterObj<TContract>,
139136
TContract extends AppRouter,
140137
>(
141138
contract: TContract,
@@ -148,15 +145,15 @@ export const initServer = () => ({
148145
requestValidationErrorHandler: 'combined',
149146
},
150147
) => {
151-
recursivelyRegisterRouter(routerImpl.routes, contract, [], app, options);
148+
recursivelyRegisterRouter(routerImpl, contract, [], app, options);
152149

153150
app.setErrorHandler(
154151
requestValidationErrorHandler(options.requestValidationErrorHandler),
155152
);
156153
},
157154
plugin:
158155
<T extends AppRouter>(
159-
router: InitialisedRouter<T>,
156+
router: RecursiveRouterObj<T>,
160157
): fastify.FastifyPluginCallback<RegisterRouterOptions> =>
161158
(
162159
app,
@@ -168,7 +165,11 @@ export const initServer = () => ({
168165
},
169166
done,
170167
) => {
171-
recursivelyRegisterRouter(router.routes, router.contract, [], app, opts);
168+
const embeddedContract = (
169+
router as RecursiveRouterObj<T> & { [RouterEmbeddedContract]: T }
170+
)[RouterEmbeddedContract];
171+
172+
recursivelyRegisterRouter(router, embeddedContract, [], app, opts);
172173

173174
app.setErrorHandler(
174175
requestValidationErrorHandler(opts.requestValidationErrorHandler),
@@ -289,12 +290,6 @@ const registerRoute = <TAppRoute extends AppRoute>(
289290
});
290291
};
291292

292-
const implementationIsInitialisedRouter = <T extends AppRouter>(
293-
implementation: InitialisedRouter<T> | RecursiveRouterObj<T>,
294-
): implementation is InitialisedRouter<T> => {
295-
return 'contract' in implementation && 'routes' in implementation;
296-
};
297-
298293
/**
299294
*
300295
* @param routerImpl - the user's implementation of the router
@@ -311,24 +306,14 @@ const recursivelyRegisterRouter = <T extends AppRouter>(
311306
options: RegisterRouterOptions,
312307
) => {
313308
if (typeof routerImpl === 'object') {
314-
if (implementationIsInitialisedRouter(routerImpl)) {
309+
for (const key in routerImpl) {
315310
recursivelyRegisterRouter(
316-
routerImpl.routes,
317-
routerImpl.contract,
318-
[...path],
311+
routerImpl[key] as unknown as RecursiveRouterObj<T>,
312+
appRouter[key] as unknown as T,
313+
[...path, key],
319314
fastify,
320315
options,
321316
);
322-
} else {
323-
for (const key in routerImpl) {
324-
recursivelyRegisterRouter(
325-
routerImpl[key] as unknown as RecursiveRouterObj<T>,
326-
appRouter[key] as unknown as T,
327-
[...path, key],
328-
fastify,
329-
options,
330-
);
331-
}
332317
}
333318
} else if (typeof routerImpl === 'function') {
334319
registerRoute(

0 commit comments

Comments
 (0)