API Route Handlers and Middleware
Unanswered
Broad-snouted Caiman posted this in #help-forum
Broad-snouted CaimanOP
I'm working on a new Next.js 13.4.x project and I want to utilize route handlers in the /app directory. However, I'm having a really hard time understanding how to translate API routes from the /pages directory to /app. In /app it seems as though you must return a Response or NextResponse object in order to send a response to the client, unlike in /pages where you could simply call res.end() or res.json() to trigger a response. This made things like reusable API middleware easy to implement.
That being said, how does that translate into the /app directory? This seems difficult, as the http response must be explicitly returned and we cannot simply call NextResponse.json() to trigger a response. Has anyone else figured this out? This is sort of making me hesitant on migrating my API routes to the /app directory...
Here is an example of what I'm trying to do, but in the /app directory: https://github.com/vercel/next.js/blob/canary/examples/api-routes-cors/pages/api/cors.ts
That being said, how does that translate into the /app directory? This seems difficult, as the http response must be explicitly returned and we cannot simply call NextResponse.json() to trigger a response. Has anyone else figured this out? This is sort of making me hesitant on migrating my API routes to the /app directory...
Here is an example of what I'm trying to do, but in the /app directory: https://github.com/vercel/next.js/blob/canary/examples/api-routes-cors/pages/api/cors.ts
32 Replies
@Broad-snouted Caiman I'm working on a new Next.js 13.4.x project and I want to utilize route handlers in the /app directory. However, I'm having a really hard time understanding how to translate API routes from the /pages directory to /app. In /app it seems as though you must return a Response or NextResponse object in order to send a response to the client, unlike in /pages where you could simply call res.end() or res.json() to trigger a response. This made things like reusable API middleware easy to implement.
That being said, how does that translate into the /app directory? This seems difficult, as the http response must be explicitly returned and we cannot simply call NextResponse.json() to trigger a response. Has anyone else figured this out? This is sort of making me hesitant on migrating my API routes to the /app directory...
Here is an example of what I'm trying to do, but in the /app directory: https://github.com/vercel/next.js/blob/canary/examples/api-routes-cors/pages/api/cors.ts
you could basically just
export async function GET(
req: NextRequest,
) {
const res = new Response()
// Run the middleware
await runMiddleware(req, res, cors)
// Rest of the API logic
return res.json({ message: 'Hello Everyone!' })
}though further configuration may be needed to appropriately read
NextRequest and NextResponseBroad-snouted CaimanOP
I feel like this is a dumb question, but what if the middleware returns a Response? I don't think the middleware's response would be sent, would it? I feel like I tried that before and it didn't seem to work.
then.. you could
const res = runMiddleware(req, new Response(), cors) ?Broad-snouted CaimanOP
I think so, yes. But then, I think that kind of defeats the purpose of middleware, right? Because in each API route, you'd have to intercept the
res object, check if it is defined, and then forward that response by returning it.you could also make it like this pattern if you prefer functional
export async function GET(
req: NextRequest,
) {
return withMiddleware(() => {
return NextResponse.json({ message: 'Hello Everyone!' })
})
}the res object is no longer exist from the beginning. The app dir is moving away from the classic express res chaining.
Broad-snouted CaimanOP
I think the Express-style
res object is really powerful because you can practically call res.send() or .end() from virtually any middleware or route and the response is sent to the client. So I suppose since the underlying API is changing, that is no longer possible and we'll need to adopt new patterns like what you're proposing?well in here you could too, just return
NextResponse at any given timeor
throw an error in the inner callback, which will be caught and sent an appropriate pre-defined response
but yeah i guess so
its a different paradigm, as mentioned by some people here :/
Im trying to say that the mental model is more or less still the same, but only the implementation are a bit different. but i guess some people are just so used to it xD
Broad-snouted CaimanOP
So are you saying any middleware that would traditionally return a response (i.e. parsing payloads, checking auth headers) would need to throw instead of return? One nice thing about the old paradigm was I could use something like zod via a middleware to parse the request body and return a 400 response from within the middleware directly. That way, I knew as long as I called that middleware before my route handler I would have a valid payload.
you could do that
function withMiddleware(cb){
const isAuth = await getAuth()
if(!isAuth) return new Response(null, { status: 401 })
return cb()
}
export async function GET(
req: NextRequest,
) {
return withMiddleware(() => {
return NextResponse.json({ message: 'Hello Everyone!' })
})
}see? you could return it
Broad-snouted CaimanOP
I see. Thank you for the example. Would that look similar if my API route needed to execute multiple middleware? Maybe a logging middleware that logs the request time and an auth middleware after?
I saw this in the App Router docs, but I don't know if it really applies here or not: https://nextjs.org/docs/app/api-reference/functions/next-response#next
Broad-snouted CaimanOP
As far as "chaining"/"stacking" the middleware, I'm assuming it would look something like this?
export async function GET(req: NextRequest){
return withLogger(withAuth((req, event) => {...}));
}I stumbled across this vid yesterday and it kind of covered this, although it was really intended for the
middleware.ts file: https://www.youtube.com/watch?v=fmFYH_Xu3d0@Broad-snouted Caiman As far as "chaining"/"stacking" the middleware, I'm assuming it would look something like this?
export async function GET(req: NextRequest){
return withLogger(withAuth((req, event) => {...}));
}
If it doesnt return any response you could just do it procedurally/linearly ðŸ«
Broad-snouted CaimanOP
Word. I'm gonna test this out a bit more and if I find a solution I'll post back here. Thanks a lot for the help!
Broad-snouted CaimanOP
Alright so I literally spent an entire day tinkering with this and here are my takeaways:
1. The next-connect package is the way to go if you want to do this using route handlers in the /app directory. Easy to use, relatively easy to understand. Be sure to read the “gotchas†in the docs, though.
2. The API folder in the /pages dir simply has more examples to pull from + has nice features like event listeners on the res object.
3. If you truly want to stay on the bleeding edge and go all-in on /app, give next-connect a shot. If you just need something functional and don’t know much about native web Request and Response (like me), just use /pages/api. I’m starting a new project and I’ve decided to use the /app directory for everything but the public REST API, which I’m keeping in /pages/api. Thankfully, the Next team has seemingly committed to ongoing support and development for the /pages directory so I feel confident that it will not be going away anytime soon.
4 (bonus): I used Cal.com’s GitHub repo as a reference for creating highly-extensible and reusable middleware. If you’re like me and need guidance on this, I cannot recommend their repo enough. Kudos to the Cal.com team for the transparency and allowing other developers to also benefit from their hard work.
References:
https://github.com/hoangvvo/next-connect
https://github.com/calcom/cal.com
1. The next-connect package is the way to go if you want to do this using route handlers in the /app directory. Easy to use, relatively easy to understand. Be sure to read the “gotchas†in the docs, though.
2. The API folder in the /pages dir simply has more examples to pull from + has nice features like event listeners on the res object.
3. If you truly want to stay on the bleeding edge and go all-in on /app, give next-connect a shot. If you just need something functional and don’t know much about native web Request and Response (like me), just use /pages/api. I’m starting a new project and I’ve decided to use the /app directory for everything but the public REST API, which I’m keeping in /pages/api. Thankfully, the Next team has seemingly committed to ongoing support and development for the /pages directory so I feel confident that it will not be going away anytime soon.
4 (bonus): I used Cal.com’s GitHub repo as a reference for creating highly-extensible and reusable middleware. If you’re like me and need guidance on this, I cannot recommend their repo enough. Kudos to the Cal.com team for the transparency and allowing other developers to also benefit from their hard work.
References:
https://github.com/hoangvvo/next-connect
https://github.com/calcom/cal.com
Broad-snouted CaimanOP
Just wanted to post an update here quickly– I sort of managed to get this working with route handlers using 0 dependencies (I am using chalk and Zod in this example, but those are completely optional): https://gist.github.com/nick-cheatwood7/68a127ce79d04fee35bedeb44157b4d3
this results in a reusable function that you can call like so:
// src/app/api/health/route.ts
import { withGlobalMiddleware, withMiddleware } from "@/lib/middleware";
const handler = () => {
return new Response("OK");
};
export const GET = withGlobalMiddleware(handler);
// or
export const GET = withMiddleware([...], handler);