N
Nuxt3d ago
Sneruz

Reducing Redundant API Handlers While Preserving TypeScript Types

I have multiple handlers that are nearly identical except for the types they use and the endpoint paths. Here's a simplified example:
// types.ts
export interface RequestTypeA { /* fields */ }
export interface ResponseTypeA { /* fields */ }

export interface RequestTypeB { /* fields */ }
export interface ResponseTypeB { /* fields */ }

export interface RequestTypeC { /* fields */ }
export interface ResponseTypeC { /* fields */ }

// server/api/A.get.ts
import { RequestTypeA, ResponseTypeA } from '@/types';

export default defineEventHandler(async (event) => {
const params = getQuery(event) as RequestTypeA;
const data = await $fetch<ResponseTypeA>('/api/A', { params });
return data;
});

// server/api/B.get.ts
import { RequestTypeB, ResponseTypeB } from '@/types';

export default defineEventHandler(async (event) => {
const params = getQuery(event) as RequestTypeB;
const data = await $fetch<ResponseTypeB>('/api/B', { params });
return data;
});

// server/api/C.get.ts
import { RequestTypeC, ResponseTypeC } from '@/types';

export default defineEventHandler(async (event) => {
const params = getQuery(event) as RequestTypeC;
const data = await $fetch<ResponseTypeC>('/api/C', { params });
return data;
});
// types.ts
export interface RequestTypeA { /* fields */ }
export interface ResponseTypeA { /* fields */ }

export interface RequestTypeB { /* fields */ }
export interface ResponseTypeB { /* fields */ }

export interface RequestTypeC { /* fields */ }
export interface ResponseTypeC { /* fields */ }

// server/api/A.get.ts
import { RequestTypeA, ResponseTypeA } from '@/types';

export default defineEventHandler(async (event) => {
const params = getQuery(event) as RequestTypeA;
const data = await $fetch<ResponseTypeA>('/api/A', { params });
return data;
});

// server/api/B.get.ts
import { RequestTypeB, ResponseTypeB } from '@/types';

export default defineEventHandler(async (event) => {
const params = getQuery(event) as RequestTypeB;
const data = await $fetch<ResponseTypeB>('/api/B', { params });
return data;
});

// server/api/C.get.ts
import { RequestTypeC, ResponseTypeC } from '@/types';

export default defineEventHandler(async (event) => {
const params = getQuery(event) as RequestTypeC;
const data = await $fetch<ResponseTypeC>('/api/C', { params });
return data;
});
As you can see, the event handlers for A, B, and C are very similar, with only the types and endpoint paths being different. What I've Tried: I attempted to reduce redundancy by using dynamic route parameters:
// server/api/[endpoint].get.ts
export default defineEventHandler(async (event) => {
const endpoint = getRouteParam(event, 'endpoint')
const params = getQuery(event);
const data = await $fetch(`/api/${endpoint}`, { params });
return data;
});
// server/api/[endpoint].get.ts
export default defineEventHandler(async (event) => {
const endpoint = getRouteParam(event, 'endpoint')
const params = getQuery(event);
const data = await $fetch(`/api/${endpoint}`, { params });
return data;
});
While this simplifies the code, I'm losing the specific TypeScript types for each request and response. My Question: Is there a way to eliminate the redundancy in my API handlers while maintaining specific TypeScript types for each endpoint?
2 Replies
kapa.ai
kapa.ai3d ago
Hi @Sneruz. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave: ---------------------- To address your question about reducing redundancy in API handlers while preserving TypeScript types, we can leverage Nuxt 3's type system and some advanced TypeScript features. Here's an approach you can consider: 1. First, let's create a type map for your endpoints:
// types.ts
export interface RequestTypeA { /* fields */ }
export interface ResponseTypeA { /* fields */ }

export interface RequestTypeB { /* fields */ }
export interface ResponseTypeB { /* fields */ }

export interface RequestTypeC { /* fields */ }
export interface ResponseTypeC { /* fields */ }

export interface EndpointMap {
A: { request: RequestTypeA; response: ResponseTypeA };
B: { request: RequestTypeB; response: ResponseTypeB };
C: { request: RequestTypeC; response: ResponseTypeC };
}
// types.ts
export interface RequestTypeA { /* fields */ }
export interface ResponseTypeA { /* fields */ }

export interface RequestTypeB { /* fields */ }
export interface ResponseTypeB { /* fields */ }

export interface RequestTypeC { /* fields */ }
export interface ResponseTypeC { /* fields */ }

export interface EndpointMap {
A: { request: RequestTypeA; response: ResponseTypeA };
B: { request: RequestTypeB; response: ResponseTypeB };
C: { request: RequestTypeC; response: ResponseTypeC };
}
2. Now, we can create a generic handler that uses this type map:
// server/api/[endpoint].get.ts
import { EndpointMap } from '~/types'

export default defineEventHandler(async (event) => {
const endpoint = getRouterParam(event, 'endpoint') as keyof EndpointMap
const query = getValidatedQuery(event, (data): data is EndpointMap[typeof endpoint]['request'] => {
// Implement your validation logic here
return true
})

const data = await $fetch<EndpointMap[typeof endpoint]['response']>(`/api/${endpoint}`, { params: query })
return data
})
// server/api/[endpoint].get.ts
import { EndpointMap } from '~/types'

export default defineEventHandler(async (event) => {
const endpoint = getRouterParam(event, 'endpoint') as keyof EndpointMap
const query = getValidatedQuery(event, (data): data is EndpointMap[typeof endpoint]['request'] => {
// Implement your validation logic here
return true
})

const data = await $fetch<EndpointMap[typeof endpoint]['response']>(`/api/${endpoint}`, { params: query })
return data
})
This approach uses several advanced features: 1. We use getRouterParam to get the endpoint parameter, as shown in the h3 documentation. 2. We use getValidatedQuery for type-safe query parameter validation, as recommended in the h3 documentation on data validation. 3. We use TypeScript's indexed access types to ensure that the types for each endpoint are correctly inferred. This solution maintains type safety while reducing code duplication. The TypeScript compiler will ensure that the correct types are used for each endpoint. Note that you'll need to implement proper validation logic in the getValidatedQuery call to ensure runtime type safety. You might want to use a validation library like Zod for this purpose. Remember to adjust your nuxt.config.ts to enable experimental features if you haven't already:
export default defineNuxtConfig({
experimental: {
typedPages: true
}
})
export default defineNuxtConfig({
experimental: {
typedPages: true
}
})
This approach should give you a good balance between code reuse and type safety. However, be aware that this level of dynamic typing might make your code a bit more complex and harder to understand at first glance. Make sure to add good documentation to explain how it works. Nuxt 3 Documentation on TypeScript h3 Documentation on Route Parameters h3 Documentation on Data Validation ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai3d ago
<@727171126458843246> Kapa.ai is still learning and improving, please let me know how I did by reacting below.

Did you find this page helpful?