@@ -11,7 +11,22 @@ import Macroable from '@poppinss/macroable'
1111import type { Assert } from '@japa/assert'
1212
1313import { ApiRequest } from './request.js'
14- import { type SetupHandler , type TeardownHandler , type CookiesSerializer } from './types.js'
14+ import {
15+ type SetupHandler ,
16+ type TeardownHandler ,
17+ type CookiesSerializer ,
18+ type InferBody ,
19+ type InferResponse ,
20+ type InferQuery ,
21+ type RoutesRegistry ,
22+ type PatternSerializer ,
23+ type UserRoutesRegistry ,
24+ type InferRouteBody ,
25+ type InferRouteQuery ,
26+ type InferRouteResponse ,
27+ type InferRouteParams ,
28+ type IsEmptyObject ,
29+ } from './types.js'
1530
1631/**
1732 * ApiClient exposes the API to make HTTP requests in context of
@@ -36,6 +51,18 @@ export class ApiClient extends Macroable {
3651
3752 static #customCookiesSerializer?: CookiesSerializer
3853
54+ /**
55+ * Routes registry for type-safe named routes
56+ */
57+ static #routesRegistry?: RoutesRegistry
58+
59+ /**
60+ * Pattern serializer for converting patterns to URLs
61+ */
62+ static #patternSerializer: PatternSerializer = ( pattern , params ) => {
63+ return pattern . replace ( / : ( \w + ) / g, ( _ , key ) => String ( params [ key ] ?? '' ) )
64+ }
65+
3966 #baseUrl?: string
4067 #assert?: Assert
4168
@@ -104,6 +131,30 @@ export class ApiClient extends Macroable {
104131 return this
105132 }
106133
134+ /**
135+ * Register a routes registry for type-safe named routes
136+ */
137+ static setRoutes ( registry : RoutesRegistry ) {
138+ this . #routesRegistry = registry
139+ return this
140+ }
141+
142+ /**
143+ * Register a custom pattern serializer
144+ */
145+ static setPatternSerializer ( serializer : PatternSerializer ) {
146+ this . #patternSerializer = serializer
147+ return this
148+ }
149+
150+ /**
151+ * Clear the routes registry
152+ */
153+ static clearRoutes ( ) {
154+ this . #routesRegistry = undefined
155+ return this
156+ }
157+
107158 /**
108159 * Create an instance of the request
109160 */
@@ -142,49 +193,101 @@ export class ApiClient extends Macroable {
142193 /**
143194 * Create an instance of the request for GET method
144195 */
145- get ( endpoint : string ) {
146- return this . request ( endpoint , 'GET' )
196+ get < P extends string > ( endpoint : P ) : ApiRequest < never , InferResponse < P > , InferQuery < P > > {
197+ return this . request ( endpoint , 'GET' ) as ApiRequest < never , InferResponse < P > , InferQuery < P > >
147198 }
148199
149200 /**
150201 * Create an instance of the request for POST method
151202 */
152- post ( endpoint : string ) {
153- return this . request ( endpoint , 'POST' )
203+ post < P extends string > ( endpoint : P ) : ApiRequest < InferBody < P > , InferResponse < P > , InferQuery < P > > {
204+ return this . request ( endpoint , 'POST' ) as ApiRequest <
205+ InferBody < P > ,
206+ InferResponse < P > ,
207+ InferQuery < P >
208+ >
154209 }
155210
156211 /**
157212 * Create an instance of the request for PUT method
158213 */
159- put ( endpoint : string ) {
160- return this . request ( endpoint , 'PUT' )
214+ put < P extends string > ( endpoint : P ) : ApiRequest < InferBody < P > , InferResponse < P > , InferQuery < P > > {
215+ return this . request ( endpoint , 'PUT' ) as ApiRequest <
216+ InferBody < P > ,
217+ InferResponse < P > ,
218+ InferQuery < P >
219+ >
161220 }
162221
163222 /**
164223 * Create an instance of the request for PATCH method
165224 */
166- patch ( endpoint : string ) {
167- return this . request ( endpoint , 'PATCH' )
225+ patch < P extends string > ( endpoint : P ) : ApiRequest < InferBody < P > , InferResponse < P > , InferQuery < P > > {
226+ return this . request ( endpoint , 'PATCH' ) as ApiRequest <
227+ InferBody < P > ,
228+ InferResponse < P > ,
229+ InferQuery < P >
230+ >
168231 }
169232
170233 /**
171234 * Create an instance of the request for DELETE method
172235 */
173- delete ( endpoint : string ) {
174- return this . request ( endpoint , 'DELETE' )
236+ delete < P extends string > ( endpoint : P ) : ApiRequest < InferBody < P > , InferResponse < P > , InferQuery < P > > {
237+ return this . request ( endpoint , 'DELETE' ) as ApiRequest <
238+ InferBody < P > ,
239+ InferResponse < P > ,
240+ InferQuery < P >
241+ >
175242 }
176243
177244 /**
178245 * Create an instance of the request for HEAD method
179246 */
180- head ( endpoint : string ) {
181- return this . request ( endpoint , 'HEAD' )
247+ head < P extends string > ( endpoint : P ) : ApiRequest < never , InferResponse < P > , InferQuery < P > > {
248+ return this . request ( endpoint , 'HEAD' ) as ApiRequest < never , InferResponse < P > , InferQuery < P > >
182249 }
183250
184251 /**
185252 * Create an instance of the request for OPTIONS method
186253 */
187- options ( endpoint : string ) {
188- return this . request ( endpoint , 'OPTIONS' )
254+ options < P extends string > ( endpoint : P ) : ApiRequest < never , InferResponse < P > , InferQuery < P > > {
255+ return this . request ( endpoint , 'OPTIONS' ) as ApiRequest < never , InferResponse < P > , InferQuery < P > >
256+ }
257+
258+ /**
259+ * Create a type-safe request using a named route from the registry.
260+ * The route name must be registered in both the runtime registry
261+ * (via ApiClient.setRoutes()) and the type registry (UserRoutesRegistry).
262+ */
263+ visit < Name extends keyof UserRoutesRegistry > (
264+ ...args : IsEmptyObject < InferRouteParams < Name > > extends true
265+ ? [ name : Name ]
266+ : [ name : Name , params : InferRouteParams < Name > ]
267+ ) : ApiRequest < InferRouteBody < Name > , InferRouteResponse < Name > , InferRouteQuery < Name > > {
268+ const name = args [ 0 ]
269+ const params = ( args [ 1 ] ?? { } ) as Record < string , any >
270+
271+ const registry = ( this . constructor as typeof ApiClient ) . #routesRegistry
272+ if ( ! registry ) {
273+ throw new Error (
274+ `Routes registry not configured. Use ApiClient.routes() to register your routes.`
275+ )
276+ }
277+
278+ const routeDef = registry [ name as string ]
279+ if ( ! routeDef ) {
280+ throw new Error ( `Route "${ String ( name ) } " not found in routes registry.` )
281+ }
282+
283+ const serializer = ( this . constructor as typeof ApiClient ) . #patternSerializer
284+ const endpoint = serializer ( routeDef . pattern , params )
285+ const method = routeDef . methods [ 0 ]
286+
287+ return this . request ( endpoint , method ) as ApiRequest <
288+ InferRouteBody < Name > ,
289+ InferRouteResponse < Name > ,
290+ InferRouteQuery < Name >
291+ >
189292 }
190293}
0 commit comments