next-connect
The promise-based method routing and middleware layer for Next.js and many other frameworks.
Warning
v1 is a complete rewrite of v0 and is not backward-compatible. See Releases to learn about the changes.
Features
- Koa-like Async middleware
- Lightweight => Suitable for serverless environment
- 5x faster than Express.js with no overhead. Compatible with Express.js via a wrapper.
- Works with async handlers (with error catching)
- TypeScript support
Installation
npm install next-connect@next
Usage
Note
Although next-connect
is initially written for Next.js, it can be used in http server, Vercel. See Examples for more integrations.
Below are some use cases.
Next.js API Routes
import type { NextApiRequest, NextApiResponse } from "next";
import { createRouter } from "next-connect";
const router = createRouter<NextApiRequest, NextApiResponse>();
router
.use(async (req, res, next) => {
const start = Date.now();
await next();
const end = Date.now();
console.log(`Request took ${end - start}ms`);
})
.use(authMiddleware)
.get((req, res) => {
res.send("Hello world");
})
.post(async (req, res) => {
const user = await insertUser(req.body.user);
res.json({ user });
})
.put(
async (req, res, next) => {
if (!req.isLoggedIn) throw new Error("thrown stuff will be caught");
return next();
},
async (req, res) => {
const user = await updateUser(req.body.user);
res.json({ user });
}
);
export default router.handler({
onError: (err, req, res, next) => {
console.error(err.stack);
res.status(500).end("Something broke!");
},
onNoMatch: (req, res) => {
res.status(404).end("Page is not found");
},
});
Next.js getServerSideProps
import { createRouter } from "next-connect";
export default function Page({ user, updated }) {
return (
<div>
{updated && <p>User has been updated</p>}
<div>{JSON.stringify(user)}</div>
<form method="POST">{/* User update form */}</form>
</div>
);
}
const router = createRouter()
.use(async (req, res, next) => {
logRequest(req);
return next();
})
.get(async (req, res) => {
const user = await getUser(req.params.id);
if (!user) {
return { props: { notFound: true } };
}
return {
props: { user, updated: true },
};
})
.post(async (req, res) => {
const user = await updateUser(req);
return {
props: { user, updated: true },
};
});
export async function getServerSideProps({ req, res }) {
try {
return await router.run(req, res);
} catch (e) {
return {
props: { error: e.message },
};
}
}
API
router = createRouter(options)
Create an instance Node.js router.
options.attachParams
Passing true
will attach params object to req. By default, next-connect
does not set to req.params. If req.params
already exists, it Object.assign into req.params
.
const router = createRouter({ attachParams: true });
router.get("/users/:userId/posts/:postId", (req, res) => {
res.send(req.params);
});
router.use(base, ...fn)
base
(optional) - match all routes to the right of base
or match all if omitted. (Note: If used in Next.js, this is often omitted)
fn
(s) can either be:
- functions of
(req, res[, next])
- or a router instance
router1.use(async (req, res, next) => {
req.hello = "world";
await next();
console.log("request is done");
});
router2.use("/foo", fn);
const sub1 = createRouter().use(fn1, fn2);
const sub2 = createRouter().use("/dashboard", auth);
const sub3 = createRouter()
.use("/waldo", subby)
.get(getty)
.post("/baz", posty)
.put("/", putty);
router3
.use(sub1, sub2)
.use("/foo", sub3);
router.METHOD(pattern, ...fns)
METHOD
is an HTTP method (GET
, HEAD
, POST
, PUT
, PATCH
, DELETE
, OPTIONS
, TRACE
) in lowercase.
pattern
(optional) - match routes based on supported pattern or match any if omitted.
fn
(s) are functions of (req, res[, next])
.
router.get("/api/user", (req, res, next) => {
res.json(req.user);
});
router.post("/api/users", (req, res, next) => {
res.end("User created");
});
router.put("/api/user/:id", (req, res, next) => {
res.end(`User ${req.query.id} updated`);
});
router.get((req, res, next) => {
res.end("This matches whatever route");
});
Note
You should understand Next.js file-system based routing. For example, having a router.put("/api/foo", handler)
inside page/api/index.js
does not serve that handler at /api/foo
.
router.all(pattern, ...fns)
Same as .METHOD but accepts any methods.
router.handler(options)
Create a handler to handle incoming requests.
options.onError
Accepts a function as a catch-all error handler; executed whenever a handler throws an error.
By default, it responds with status code 500
and an error stack if any.
function onError(err, req, res) {
logger.log(err);
res.status(500).end("Internal server error");
}
export default router.handler({ onError });
Warning
The default option prints the error stack, which might be a security risk. Consider defining a custom one like the above to mitigate the risk.
options.onNoMatch
Accepts a function of (req, res)
as a handler when no route is matched.
By default, it responds with a 404
status and a Route [Method] [Url] not found
body.
function onNoMatch(req, res) {
res.status(404).end("page is not found... or is it!?");
}
export default router.handler({ onNoMatch });
router.run(req, res)
Runs req
and res
through the middleware chain and returns a promise. It resolves with the value returned from handlers.
router
.use(async (req, res, next) => {
return (await next()) + 1;
})
.use(async () => {
return (await next()) + 2;
})
.use(async () => {
return 3;
});
console.log(await router.run(req, res));
This can be useful in getServerSideProps
.
Common errors
There are some pitfalls in using next-connect
. Below are things to keep in mind to use it correctly.
- Always
await next()
If next()
is not awaited, errors will not be caught if they are thrown in async handlers, leading to UnhandledPromiseRejection
.
router
.use((req, res, next) => {
next();
})
.use((req, res, next) => {
next();
})
.use(() => {
throw new Error("💥");
});
router
.use(async (req, res, next) => {
next();
})
.use(async (req, res, next) => {
next();
})
.use(async () => {
throw new Error("💥");
});
router
.use(async (req, res, next) => {
await next();
})
.use((req, res, next) => {
return next();
})
.use(async () => {
throw new Error("💥");
});
Another issue is that the handler would resolve before all the code in each layer runs.
const handler = router
.use(async (req, res, next) => {
next();
})
.get(async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
res.send("ok");
console.log("request is completed");
})
.handler();
await handler(req, res);
console.log("finally");
- DO NOT reuse the same instance of
router
like the below pattern:
export default createRouter().use(a).use(b);
import router from "api-libs/base";
export default router.get(x);
import router from "api-libs/base";
export default router.get(y);
This is because, in each API Route, the same router instance is mutated, leading to undefined behaviors.
If you want to achieve something like that, you can use router.clone
to return different instances with the same routes populated.
export default createRouter().use(a).use(b);
import router from "api-libs/base";
export default router.clone().get(x);
import router from "api-libs/base";
export default router.clone().get(y);
- DO NOT use response function like
res.(s)end
or res.redirect
inside getServerSideProps
.
const handler = createRouter()
.use((req, res) => {
res.redirect("foo");
})
.use((req, res) => {
res.end("bar");
});
export async function getServerSideProps({ req, res }) {
await router.run(req, res);
return {
props: {},
};
}
- DO NOT use
handler()
directly in getServerSideProps
.
const router = createRouter().use(foo).use(bar);
const handler = router.handler();
export async function getServerSideProps({ req, res }) {
await handler(req, res);
return {
props: {},
};
}
Recipes
Next.js
Match multiple routes
If you created the file /api/<specific route>.js
folder, the handler will only run on that specific route.
If you need to create all handlers for all routes in one file (similar to Express.js
). You can use Optional catch-all API routes.
import { createRouter } from "next-connect";
const router = createRouter()
.use("/api/hello", someMiddleware())
.get("/api/user/:userId", (req, res) => {
res.send(`Hello ${req.params.userId}`);
});
export default router.handler();
While this allows quick migration from Express.js, consider seperating routes into different files (/api/user/[userId].js
, /api/hello.js
) in the future.
Express.js Compatibility
Middleware wrapper
Express middleware is not built around promises but callbacks. This prevents it from playing well in the next-connect
model. Understanding the way express middleware works, we can build a wrapper like the below:
import someExpressMiddleware from "some-express-middleware";
const withExpressMiddleware = (middleware) => {
return async (req, res, next) => {
await new Promise((resolve, reject) => {
middleware(req, res, (err) => (err ? reject(err) : resolve()));
});
return next();
};
};
router.use(withExpressMiddleware(someExpressMiddleware));
Contributing
Please see my contributing.md.
License
MIT