Socket
Socket
Sign inDemoInstall

next-connect

Package Overview
Dependencies
1
Maintainers
1
Versions
37
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

    next-connect

The method routing and middleware layer for Next.js (and many others)


Version published
Maintainers
1
Install size
49.0 kB
Created

Changelog

Source

1.0.0-next.0

See https://github.com/hoangvvo/next-connect/releases/tag/v1.0.0-next.0.

Readme

Source

next-connect

npm CircleCI codecov minified size download/year

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

// pages/api/hello.js
import type { NextApiRequest, NextApiResponse } from "next";
import { createRouter } from "next-connect";

// Default Req and Res are IncomingMessage and ServerResponse
// You may want to pass in NextApiRequest and NextApiResponse
const router = createRouter<NextApiRequest, NextApiResponse>();

router
  .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`);
  })
  .use(authMiddleware)
  .get((req, res) => {
    res.send("Hello world");
  })
  .post(async (req, res) => {
    // use async/await
    const user = await insertUser(req.body.user);
    res.json({ user });
  })
  .put(
    async (req, res, next) => {
      // You may want to pass in NextApiRequest & { isLoggedIn: true }
      // in createRouter generics to define this extra property
      if (!req.isLoggedIn) throw new Error("thrown stuff will be caught");
      // go to the next in chain
      return next();
    },
    async (req, res) => {
      const user = await updateUser(req.body.user);
      res.json({ user });
    }
  );

// create a handler from router with custom
// onError and onNoMatch
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

// page/users/[id].js
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) {
      // https://nextjs.org/docs/api-reference/data-fetching/get-server-side-props#notfound
      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 {
    // we await here so that the error can be caught below
    // rather than propagate to the outer layer
    return await router.run(req, res);
  } catch (e) {
    // handle the error
    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) => {
  // Visiting '/users/12/posts/23' will render '{"userId":"12","postId":"23"}'
  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
// 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 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) => {
  // https://nextjs.org/docs/routing/dynamic-routes
  res.end(`User ${req.query.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. 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);
  // OR: console.error(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));
// The above will print "6"

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.

  1. Always await next()

If next() is not awaited, errors will not be caught if they are thrown in async handlers, leading to UnhandledPromiseRejection.

// 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.

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"
  1. DO NOT reuse the same instance of router like the below pattern:
// 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);

// api/bar.js
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.

// 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);

// api/bar.js
import router from "api-libs/base";
export default router.clone().get(y);
  1. DO NOT use response function like res.(s)end or res.redirect inside getServerSideProps.
// 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: {},
  };
}
  1. DO NOT use handler() directly in getServerSideProps.
// 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: {},
  };
}

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.

// pages/api/[[...slug]].js
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

Keywords

FAQs

Last updated on 03 Jul 2022

Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc