multi-version rest api

Im using nuxt3 to serve my rest api. I have a new version of the client which consumes the API and would like to create a new 'version' of the api with some of the endpoints being the same, some being updated. The old clients still need to work so the original version needs to still be available. I realise, I can just create a 'v2' folder and copy the handlers I need to change and then reference /v2/blah where I need the newer version... but is there an elegant way to handle this? e.g. can I create a 'V2' set that 'falls back' to the V1 handlers if no V2 handler exists? this would make it easier in the client (everything is v2)
11 Replies
kapa.ai
kapa.ai6d ago
Hi @JavascriptMick. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave: ---------------------- To handle multiple versions of your REST API in Nuxt 3, you can indeed create a more elegant solution that allows for version-specific handlers while falling back to the previous version when needed. Here's an approach you can take: 1. Create separate folders for each API version (e.g., v1 and v2). 2. Implement a catch-all route handler for v2 that checks if a specific handler exists, and if not, falls back to the v1 handler. Here's how you can implement this: 1. Set up your folder structure:
server/
api/
v1/
[...]
v2/
[...].ts
server/
api/
v1/
[...]
v2/
[...].ts
2. In your server/api/v2/[...].ts file, implement a catch-all route handler:
import { defineEventHandler, createError, getRouterParams } from 'h3'
import { resolveModule } from 'local-pkg'

export default defineEventHandler(async (event) => {
const { _ } = getRouterParams(event)
const path = _ as string

// Try to resolve the v2 handler
const v2HandlerPath = `../v2/${path}`
const v2Handler = await resolveModule(v2HandlerPath).catch(() => null)

if (v2Handler) {
// If v2 handler exists, use it
return v2Handler.default(event)
}

// If v2 handler doesn't exist, try to resolve the v1 handler
const v1HandlerPath = `../v1/${path}`
const v1Handler = await resolveModule(v1HandlerPath).catch(() => null)

if (v1Handler) {
// If v1 handler exists, use it as fallback
return v1Handler.default(event)
}

// If neither v1 nor v2 handler exists, throw a 404 error
throw createError({
statusCode: 404,
statusMessage: 'API endpoint not found',
})
})
import { defineEventHandler, createError, getRouterParams } from 'h3'
import { resolveModule } from 'local-pkg'

export default defineEventHandler(async (event) => {
const { _ } = getRouterParams(event)
const path = _ as string

// Try to resolve the v2 handler
const v2HandlerPath = `../v2/${path}`
const v2Handler = await resolveModule(v2HandlerPath).catch(() => null)

if (v2Handler) {
// If v2 handler exists, use it
return v2Handler.default(event)
}

// If v2 handler doesn't exist, try to resolve the v1 handler
const v1HandlerPath = `../v1/${path}`
const v1Handler = await resolveModule(v1HandlerPath).catch(() => null)

if (v1Handler) {
// If v1 handler exists, use it as fallback
return v1Handler.default(event)
}

// If neither v1 nor v2 handler exists, throw a 404 error
throw createError({
statusCode: 404,
statusMessage: 'API endpoint not found',
})
})
This approach allows you to: 1. Create v2-specific handlers in the v2 folder when needed. 2. Automatically fall back to v1 handlers if a v2 handler doesn't exist. 3. Keep your v1 API intact for older clients. On the client side, you can now use /api/v2/... for all requests, and it will automatically use the most up-to-date handler available. This solution leverages Nuxt 3's server directory structure and the H3 server, which Nuxt 3 uses under the hood. The catch-all route **...].ts is based on the information from the [Nuxt 3 documentation on catch-all routes (https://nuxt.com/docs/guide/directory-structure/server#recipes). Remember to install the local-pkg package to use the resolveModule function:
npm install local-pkg
npm install local-pkg
This approach provides an elegant way to handle API versioning while maintaining backward compatibility and reducing code duplication.
Nuxt 3 Server Directory Structure** (https://nuxt.com/docs/guide/directory-structure/server#recipes) ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai6d ago
<@452626439099908096> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
JavascriptMick
JavascriptMickOP5d ago
@kapa.ai this doesn't work, it doesn't compile. can I do it with route rules in nuxt.config?
kapa.ai
kapa.ai5d ago
Let me check the knowledge base for you again. One moment! :books: ---------------------- I apologize for the confusion in my previous answer. You're right that the solution I provided doesn't work directly in Nuxt 3. Let's explore a more appropriate solution using route rules in nuxt.config.ts. Yes, you can use route rules in nuxt.config.ts to handle API versioning. This approach is more aligned with Nuxt 3's recommended practices. Here's how you can do it: 1. Keep your API handlers in separate version folders (v1 and v2) within the server/api directory. 2. Use routeRules in your nuxt.config.ts to set up a proxy for v2 routes that fall back to v1 when a v2 handler doesn't exist. Here's an example of how to set this up in your nuxt.config.ts:
export default defineNuxtConfig({
routeRules: {
'/api/v2/**': {
proxy: { to: '<http://localhost:3000/api/v1/**'> }
}
},
nitro: {
routeRules: {
'/api/v2/**': {
proxy: '/api/v1/**'
}
}
}
})
export default defineNuxtConfig({
routeRules: {
'/api/v2/**': {
proxy: { to: '<http://localhost:3000/api/v1/**'> }
}
},
nitro: {
routeRules: {
'/api/v2/**': {
proxy: '/api/v1/**'
}
}
}
})
This configuration does the following: 1. Any request to /api/v2/** will first try to match a v2 handler. 2. If a v2 handler doesn't exist, it will proxy the request to the corresponding v1 endpoint. This approach allows you to: 1. Create v2-specific handlers in the server/api/v2 folder when needed. 2. Automatically fall back to v1 handlers if a v2 handler doesn't exist. 3. Keep your v1 API intact for older clients. On the client side, you can now use /api/v2/... for all requests, and it will automatically use the most up-to-date handler available. This solution leverages Nuxt 3's routeRules and Nitro's proxy capabilities, which are more appropriate for handling API versioning in Nuxt 3. Nuxt 3 Route Rules Documentation (https://nuxt.com/docs/guide/concepts/rendering#hybrid-rendering) Nitro Route Rules Documentation (https://nitro.unjs.io/config/#routerules) Remember that this is a server-side solution, so it will work for server-rendered pages and API routes, but not for static site generation (SSG) scenarios. If you need more complex logic or fine-grained control over the versioning, you might need to create a custom Nitro plugin or use middleware. However, for most cases, this route rules approach should provide an elegant solution to your API versioning needs. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai5d ago
<@452626439099908096> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
JavascriptMick
JavascriptMickOP5d ago
@kapa.ai ok so this nearly works, let me explain. This route rules configuration proxys all v2 routes to the base versions...
routeRules: {
'/api/v2/**': { proxy: '/api/**' }
},
routeRules: {
'/api/v2/**': { proxy: '/api/**' }
},
But what if I have a route that is implemented differently for V2? (e.g. /api/v2/stoopid) the above configuration ensures that the V2 version of the handler is ignored. Is it possible to have a catch all like above but somehow exclude specific routes from the catch all, something like this...
routeRules: {
'/api/v2/stoopid': { proxy: 'false' },
'/api/v2/**': { proxy: '/api/**' }
},
routeRules: {
'/api/v2/stoopid': { proxy: 'false' },
'/api/v2/**': { proxy: '/api/**' }
},
Alternatively, I will need to individually specify proxies for all default routes that have no v2 version. this is simple but tedious.

routeRules: {
'/api/v2/someroute': { proxy: '/api/someroute' },
'/api/v2/someotherroute': { proxy: '/api/someotherroute' },
... etc for all routes that have no v2 version
},

routeRules: {
'/api/v2/someroute': { proxy: '/api/someroute' },
'/api/v2/someotherroute': { proxy: '/api/someotherroute' },
... etc for all routes that have no v2 version
},
Here is a discussion that I think is relevant although it is not resolved very clearly (https://github.com/nitrojs/nitro/discussions/2413) so, I guess my question is, is it possible to include a 'catch all' glob based route in nuxtConfig but also have exclusions to the catch all for specific routes that have V2 version?
GitHub
Supporting glob pattern exclusion in route rules · nitrojs nitro · ...
Hi, I have a use case where an api exists at /api/auth/ and some other endpoints starting with exactly /api/.... I would like to define an exclusion pattern to be able to proxy requests that are no...
kapa.ai
kapa.ai5d ago
Let me check the knowledge base for you again. One moment! :books: ---------------------- Based on the information provided in the knowledge sources and your specific question, it appears that there isn't a direct way to exclude specific routes from a catch-all glob-based route in Nuxt's routeRules configuration. However, we can explore a couple of approaches to achieve what you're looking for. 1. Order-based approach: The order of route rules matters in Nuxt configuration. You can leverage this by placing more specific rules before the catch-all rule. This way, specific V2 routes will be handled first, and the catch-all will apply to the rest.
export default defineNuxtConfig({
routeRules: {
'/api/v2/stoopid': {}, // This will use the V2 handler
'/api/v2/**': { proxy: '/api/**' } // This will proxy all other V2 routes to V1
}
})
export default defineNuxtConfig({
routeRules: {
'/api/v2/stoopid': {}, // This will use the V2 handler
'/api/v2/**': { proxy: '/api/**' } // This will proxy all other V2 routes to V1
}
})
In this configuration, /api/v2/stoopid will use its V2 handler, while all other V2 routes will be proxied to their V1 counterparts. 2. Nitro server handler approach: If the order-based approach doesn't provide enough flexibility, you can create a custom Nitro server handler to manage the routing logic more granularly. This approach gives you full control over which routes to proxy and which to handle directly. First, create a file server/middleware/api-version-handler.ts:
import { defineEventHandler, getRouterParams, sendProxy } from 'h3'

export default defineEventHandler(async (event) => {
const { _ } = getRouterParams(event)
const path = _ as string

// Check if the route starts with /api/v2/
if (path.startsWith('api/v2/')) {
const v2Path = `server/api/v2/${path.slice(7)}.ts`
const v1Path = `server/api/${path.slice(7)}.ts`
try {
// Try to resolve the V2 handler
await import(v2Path)
// If it exists, let Nitro handle it normally
return
} catch {
// If V2 handler doesn't exist, proxy to V1
return sendProxy(event, `<http://localhost:3000/api/${path.slice(7>)}`)
}
}
})
import { defineEventHandler, getRouterParams, sendProxy } from 'h3'

export default defineEventHandler(async (event) => {
const { _ } = getRouterParams(event)
const path = _ as string

// Check if the route starts with /api/v2/
if (path.startsWith('api/v2/')) {
const v2Path = `server/api/v2/${path.slice(7)}.ts`
const v1Path = `server/api/${path.slice(7)}.ts`
try {
// Try to resolve the V2 handler
await import(v2Path)
// If it exists, let Nitro handle it normally
return
} catch {
// If V2 handler doesn't exist, proxy to V1
return sendProxy(event, `<http://localhost:3000/api/${path.slice(7>)}`)
}
}
})
Then, in your nuxt.config.ts, add this middleware:
export default defineNuxtConfig({
nitro: {
devServerHandlers: ['~/server/middleware/api-version-handler']
}
})
export default defineNuxtConfig({
nitro: {
devServerHandlers: ['~/server/middleware/api-version-handler']
}
})
This approach allows you to have fine-grained control over which routes use V2 handlers and which fall back to V1, without needing to specify each route individually in the configuration. The GitHub discussion you mentioned Nitro discussion #2413 (https://github.com/nitrojs/nitro/discussions/2413) highlights the complexity of this issue and suggests that a more programmatic approach might be necessary for complex routing scenarios. Remember that these solutions primarily affect server-side rendering and API routes. They won't impact statically generated pages. Nuxt 3 Server Directory Structure (https://nuxt.com/docs/guide/directory-structure/server#recipes) Nuxt Configuration - devServerHandlers (https://nuxt.com/docs/api/nuxt-config#devserverhandlers) These approaches should provide you with the flexibility to handle both specific V2 routes and fall back to V1 routes when necessary, without having to individually specify proxies for all default routes. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai5d ago
<@452626439099908096> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
JavascriptMick
JavascriptMickOP4d ago
@kapa.ai none of your solutions work. I have updated the discussion here (https://github.com/nitrojs/nitro/discussions/2413#discussioncomment-11601171) in the hope of getting a response from the Nitro team
GitHub
Supporting glob pattern exclusion in route rules · nitrojs nitro · ...
Hi, I have a use case where an api exists at /api/auth/ and some other endpoints starting with exactly /api/.... I would like to define an exclusion pattern to be able to proxy requests that are no...
kapa.ai
kapa.ai4d ago
Let me check the knowledge base for you again. One moment! :books: ---------------------- I apologize that the solutions I provided didn't work for your specific case. You're right to seek clarification directly from the Nitro team, as they would have the most up-to-date and accurate information about the capabilities and limitations of their system. After reviewing the discussion you linked Nitro discussion #2413 (https://github.com/nitrojs/nitro/discussions/2413#discussioncomment-11601171), it's clear that the current implementation of routeRules in Nitro doesn't provide a straightforward way to achieve what you're looking for. The issue of having a catch-all rule with exceptions is indeed complex and not currently supported in a simple configuration. As you've correctly pointed out in your comment on the discussion, the current options seem to be: 1. Manually specify each route that needs to be proxied, which can be tedious for large APIs. 2. Use a catch-all rule, but this would override any specific V2 implementations. Unfortunately, the knowledge sources provided don't contain any additional information or solutions that could help in this specific scenario. The Nitro team's response to your comment will be crucial in understanding if there's a planned solution or if any workarounds are possible. In the meantime, if you need a working solution, you might have to resort to the manual specification of routes or consider implementing a custom server middleware that can handle the routing logic programmatically. However, these approaches come with their own complexities and maintenance overhead. I apologize again for not being able to provide a more satisfactory answer. The limitation you've encountered appears to be a genuine constraint in the current implementation of Nitro's routing system. Hopefully, the Nitro team will be able to provide more insight or consider implementing a feature to address this use case in the future. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai4d ago
<@452626439099908096> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
Want results from more Discord servers?
Add your server