Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/hoangvvo/next-connect
The TypeScript-ready, minimal router and middleware layer for Next.js, Micro, Vercel, or Node.js http/http2
https://github.com/hoangvvo/next-connect
connect javascript micro middleware nextjs router typescript
Last synced: 2 days ago
JSON representation
The TypeScript-ready, minimal router and middleware layer for Next.js, Micro, Vercel, or Node.js http/http2
- Host: GitHub
- URL: https://github.com/hoangvvo/next-connect
- Owner: hoangvvo
- License: mit
- Created: 2019-10-13T20:33:30.000Z (about 5 years ago)
- Default Branch: main
- Last Pushed: 2024-02-15T06:39:41.000Z (11 months ago)
- Last Synced: 2024-10-29T15:34:13.581Z (2 months ago)
- Topics: connect, javascript, micro, middleware, nextjs, router, typescript
- Language: TypeScript
- Homepage: https://www.npmjs.com/package/next-connect
- Size: 629 KB
- Stars: 1,637
- Watchers: 9
- Forks: 65
- Open Issues: 42
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
Awesome Lists containing this project
- awesome - hoangvvo/next-connect - The TypeScript-ready, minimal router and middleware layer for Next.js, Micro, Vercel, or Node.js http/http2 (TypeScript)
- Awesome-NextJs - next-connect - `The TypeScript-ready, minimal router and middleware layer for Next.js, Micro, Vercel, or Node.js http/http2` (miscellaneous)
- awesome-nextjs - next-connect - The Express/Connect-compatible router and middleware layer for Next.js. (Extensions)
- fucking-awesome-nextjs - next-connect - The Express/Connect-compatible router and middleware layer for Next.js. (Extensions)
README
# next-connect
[![npm](https://badgen.net/npm/v/next-connect)](https://www.npmjs.com/package/next-connect)
[![CircleCI](https://circleci.com/gh/hoangvvo/next-connect.svg?style=svg)](https://circleci.com/gh/hoangvvo/next-connect)
[![codecov](https://codecov.io/gh/hoangvvo/next-connect/branch/main/graph/badge.svg?token=LGIyl3Ti4H)](https://codecov.io/gh/hoangvvo/next-connect)
[![minified size](https://badgen.net/bundlephobia/min/next-connect)](https://bundlephobia.com/result?p=next-connect)
[![download/year](https://badgen.net/npm/dy/next-connect)](https://www.npmjs.com/package/next-connect)The promise-based method routing and middleware layer for [Next.js](https://nextjs.org/) [API Routes](#nextjs-api-routes), [Edge API Routes](#nextjs-edge-api-routes), [Middleware](#nextjs-middleware), [Next.js App Router](#nextjs-app-router), and [getServerSideProps](#nextjs-getserversideprops).
## Features
- Async middleware
- [Lightweight](https://bundlephobia.com/scan-results?packages=express,next-connect,koa,micro) => Suitable for serverless environment
- [way faster](https://github.com/hoangvvo/next-connect/tree/main/bench) than Express.js. Compatible with Express.js via [a wrapper](#expressjs-compatibility).
- Works with async handlers (with error catching)
- TypeScript support## Installation
```sh
npm install next-connect@next
```## Usage
Also check out the [examples](./examples/) folder.
### Next.js API Routes
`next-connect` can be used in [API Routes](https://nextjs.org/docs/api-routes/introduction).
```typescript
// pages/api/user/[id].ts
import type { NextApiRequest, NextApiResponse } from "next";
import { createRouter, expressWrapper } from "next-connect";
import cors from "cors";const router = createRouter();
router
// Use express middleware in next-connect with expressWrapper function
.use(expressWrapper(passport.session()))
// A middleware example
.use(async (req, res, next) => {
const start = Date.now();
await next(); // call next in chain
const end = Date.now();
console.log(`Request took ${end - start}ms`);
})
.get((req, res) => {
const user = getUser(req.query.id);
res.json({ user });
})
.put((req, res) => {
if (req.user.id !== req.query.id) {
throw new ForbiddenError("You can't update other user's profile");
}
const user = await updateUser(req.body.user);
res.json({ user });
});export const config = {
runtime: "edge",
};export default router.handler({
onError: (err, req, res) => {
console.error(err.stack);
res.status(err.statusCode || 500).end(err.message);
},
});
```### Next.js Edge API Routes
`next-connect` can be used in [Edge API Routes](https://nextjs.org/docs/api-routes/edge-api-routes)
```typescript
// pages/api/user/[id].ts
import type { NextFetchEvent, NextRequest } from "next/server";
import { createEdgeRouter } from "next-connect";
import cors from "cors";const router = createEdgeRouter();
router
// A middleware example
.use(async (req, event, next) => {
const start = Date.now();
await next(); // call next in chain
const end = Date.now();
console.log(`Request took ${end - start}ms`);
})
.get((req) => {
const id = req.nextUrl.searchParams.get("id");
const user = getUser(id);
return NextResponse.json({ user });
})
.put((req) => {
const id = req.nextUrl.searchParams.get("id");
if (req.user.id !== id) {
throw new ForbiddenError("You can't update other user's profile");
}
const user = await updateUser(req.body.user);
return NextResponse.json({ user });
});export default router.handler({
onError: (err, req, event) => {
console.error(err.stack);
return new NextResponse("Something broke!", {
status: err.statusCode || 500,
});
},
});
```### Next.js App Router
`next-connect` can be used in [Next.js 13 Route Handler](https://beta.nextjs.org/docs/routing/route-handlers). The way handlers are written is almost the same to [Next.js Edge API Routes](#nextjs-edge-api-routes) by using `createEdgeRouter`.
```typescript
// app/api/user/[id]/route.tsimport type { NextFetchEvent, NextRequest } from "next/server";
import { createEdgeRouter } from "next-connect";
import cors from "cors";interface RequestContext {
params: {
id: string;
};
}const router = createEdgeRouter();
router
// A middleware example
.use(async (req, event, next) => {
const start = Date.now();
await next(); // call next in chain
const end = Date.now();
console.log(`Request took ${end - start}ms`);
})
.get((req) => {
const id = req.params.id;
const user = getUser(id);
return NextResponse.json({ user });
})
.put((req) => {
const id = req.params.id;
if (req.user.id !== id) {
throw new ForbiddenError("You can't update other user's profile");
}
const user = await updateUser(req.body.user);
return NextResponse.json({ user });
});export async function GET(request: NextRequest, ctx: RequestContext) {
return router.run(request, ctx);
}export async function PUT(request: NextRequest, ctx: RequestContext) {
return router.run(request, ctx);
}
```### Next.js Middleware
`next-connect` can be used in [Next.js Middleware](https://nextjs.org/docs/advanced-features/middleware)
```typescript
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest, NextFetchEvent } from "next/server";
import { createEdgeRouter } from "next-connect";const router = createEdgeRouter();
router.use(async (request, event, next) => {
// logging request example
console.log(`${request.method} ${request.url}`);
return next();
});router.get("/about", (request) => {
return NextResponse.redirect(new URL("/about-2", request.url));
});router.use("/dashboard", (request) => {
if (!isAuthenticated(request)) {
return NextResponse.redirect(new URL("/login", request.url));
}
return NextResponse.next();
});router.all(() => {
// default if none of the above matches
return NextResponse.next();
});export function middleware(request: NextRequest, event: NextFetchEvent) {
return router.run(request, event);
}export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
*/
"/((?!api|_next/static|_next/image|favicon.ico).*)",
],
};
```### Next.js getServerSideProps
`next-connect` can be used in [getServerSideProps](https://nextjs.org/docs/basic-features/data-fetching/get-server-side-props).
```jsx
// pages/users/[id].js
import { createRouter } from "next-connect";export default function Page({ user, updated }) {
return (
{updated &&User has been updated
}
{JSON.stringify(user)}
{/* User update form */}
);
}const router = createRouter()
.use(async (req, res, next) => {
// this serve as the error handling middleware
try {
return await next();
} catch (e) {
return {
props: { error: e.message },
};
}
})
.get(async (req, res) => {
const user = await getUser(req.params.id);
if (!user) {
// https://nextjs.org/docs/api-reference/data-fetching/get-server-side-props#notfound
return { props: { notFound: true } };
}
return { props: { user } };
})
.put(async (req, res) => {
const user = await updateUser(req);
return { props: { user, updated: true } };
});export async function getServerSideProps({ req, res }) {
return router.run(req, res);
}
```## API
The following APIs are rewritten in term of `NodeRouter` (`createRouter`), but they apply to `EdgeRouter` (`createEdgeRouter`) as well.
### router = createRouter()
Create an instance Node.js router.
### 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```javascript
// Mount a middleware function
router1.use(async (req, res, next) => {
req.hello = "world";
await next(); // call to proceed to the next in chain
console.log("request is done"); // call after all downstream handler has run
});// Or include a base
router2.use("/foo", fn); // Only run in /foo/**// mount an instance of router
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
// - fn1 and fn2 always run
// - auth runs only on /dashboard
.use(sub1, sub2)
// `subby` runs on ANY /foo/waldo?/*
// `getty` runs on GET /foo/*
// `posty` runs on POST /foo/baz
// `putty` runs on PUT /foo
.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](https://github.com/lukeed/regexparam#regexparam-) or match any if omitted.
`fn`(s) are functions of `(req, res[, next])`.
```javascript
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) => {
// https://nextjs.org/docs/routing/dynamic-routes
res.end(`User ${req.params.id} updated`);
});// Next.js already handles routing (including dynamic routes), we often
// omit `pattern` in `.METHOD`
router.get((req, res, next) => {
res.end("This matches whatever route");
});
```> **Note**
> You should understand Next.js [file-system based routing](https://nextjs.org/docs/routing/introduction). 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](#methodpattern-fns) 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 a generic `500 Internal Server Error` while logging the error to `console`.```javascript
function onError(err, req, res) {
logger.log(err);
// OR: console.error(err);res.status(500).end("Internal server error");
}export default router.handler({ onError });
```**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.```javascript
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.
```js
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));
// The above will print "6"
```If an error in thrown within the chain, `router.run` will reject. You can also add a try-catch in the first middleware to catch the error before it rejects the `.run()` call:
```js
router
.use(async (req, res, next) => {
return next().catch(errorHandler);
})
.use(thisMiddlewareMightThrow);await router.run(req, res);
```## Common errors
There are some pitfalls in using `next-connect`. Below are things to keep in mind to use it correctly.
1. **Always** `await next()`
If `next()` is not awaited, errors will not be caught if they are thrown in async handlers, leading to `UnhandledPromiseRejection`.
```javascript
// OK: we don't use async so no need to await
router
.use((req, res, next) => {
next();
})
.use((req, res, next) => {
next();
})
.use(() => {
throw new Error("💥");
});// BAD: This will lead to UnhandledPromiseRejection
router
.use(async (req, res, next) => {
next();
})
.use(async (req, res, next) => {
next();
})
.use(async () => {
throw new Error("💥");
});// GOOD
router
.use(async (req, res, next) => {
await next(); // next() is awaited, so errors are caught properly
})
.use((req, res, next) => {
return next(); // this works as well since we forward the rejected promise
})
.use(async () => {
throw new Error("💥");
// return new Promise.reject("💥");
});
```Another issue is that the handler would resolve before all the code in each layer runs.
```javascript
const handler = router
.use(async (req, res, next) => {
next(); // this is not returned or await
})
.get(async () => {
// simulate a long task
await new Promise((resolve) => setTimeout(resolve, 1000));
res.send("ok");
console.log("request is completed");
})
.handler();await handler(req, res);
console.log("finally"); // this will run before the get layer gets to finish// This will result in:
// 1) "finally"
// 2) "request is completed"
```2. **DO NOT** reuse the same instance of `router` like the below pattern:
```javascript
// api-libs/base.js
export default createRouter().use(a).use(b);// api/foo.js
import router from "api-libs/base";
export default router.get(x).handler();// api/bar.js
import router from "api-libs/base";
export default router.get(y).handler();
```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.```javascript
// api-libs/base.js
export default createRouter().use(a).use(b);// api/foo.js
import router from "api-libs/base";
export default router.clone().get(x).handler();// api/bar.js
import router from "api-libs/base";
export default router.clone().get(y).handler();
```3. **DO NOT** use response function like `res.(s)end` or `res.redirect` inside `getServerSideProps`.
```javascript
// page/index.js
const handler = createRouter()
.use((req, res) => {
// BAD: res.redirect is not a function (not defined in `getServerSideProps`)
// See https://github.com/hoangvvo/next-connect/issues/194#issuecomment-1172961741 for a solution
res.redirect("foo");
})
.use((req, res) => {
// BAD: `getServerSideProps` gives undefined behavior if we try to send a response
res.end("bar");
});export async function getServerSideProps({ req, res }) {
await router.run(req, res);
return {
props: {},
};
}
```3. **DO NOT** use `handler()` directly in `getServerSideProps`.
```javascript
// page/index.js
const router = createRouter().use(foo).use(bar);
const handler = router.handler();export async function getServerSideProps({ req, res }) {
await handler(req, res); // BAD: You should call router.run(req, res);
return {
props: {},
};
}
```## Contributing
Please see my [contributing.md](CONTRIBUTING.md).
## License
[MIT](LICENSE)