Coder Social home page Coder Social logo

hoangvvo / next-connect Goto Github PK

View Code? Open in Web Editor NEW
1.6K 9.0 63.0 644 KB

The TypeScript-ready, minimal router and middleware layer for Next.js, Micro, Vercel, or Node.js http/http2

Home Page: https://www.npmjs.com/package/next-connect

License: MIT License

JavaScript 1.58% TypeScript 97.72% Shell 0.70%
javascript nextjs middleware connect router micro typescript

next-connect's Introduction

next-connect

npm CircleCI codecov minified size download/year

The promise-based method routing and middleware layer for Next.js API Routes, Edge API Routes, Middleware, Next.js App Router, and getServerSideProps.

Features

  • Async middleware
  • Lightweight => Suitable for serverless environment
  • way faster than Express.js. Compatible with Express.js via a wrapper.
  • Works with async handlers (with error catching)
  • TypeScript support

Installation

npm install next-connect@next

Usage

Also check out the examples folder.

Next.js API Routes

next-connect can be used in API Routes.

// pages/api/user/[id].ts
import type { NextApiRequest, NextApiResponse } from "next";
import { createRouter, expressWrapper } from "next-connect";
import cors from "cors";

const router = createRouter<NextApiRequest, NextApiResponse>();

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

// pages/api/user/[id].ts
import type { NextFetchEvent, NextRequest } from "next/server";
import { createEdgeRouter } from "next-connect";
import cors from "cors";

const router = createEdgeRouter<NextRequest, NextFetchEvent>();

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. The way handlers are written is almost the same to Next.js Edge API Routes by using createEdgeRouter.

// app/api/user/[id]/route.ts

import type { NextFetchEvent, NextRequest } from "next/server";
import { createEdgeRouter } from "next-connect";
import cors from "cors";

interface RequestContext {
  params: {
    id: string;
  };
}

const router = createEdgeRouter<NextRequest, RequestContext>();

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

// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest, NextFetchEvent } from "next/server";
import { createEdgeRouter } from "next-connect";

const router = createEdgeRouter<NextRequest, NextFetchEvent>();

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.

// pages/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) => {
    // 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
// 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.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. 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 a generic 500 Internal Server Error while logging the error to console.

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.

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"

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:

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.

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

// 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();
  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: {},
  };
}

Contributing

Please see my contributing.md.

License

MIT

next-connect's People

Contributors

dependabot-preview[bot] avatar hoangvvo avatar jasonmore avatar lamualfa avatar ljosberinn avatar manak1 avatar mmillies avatar mscottford avatar slaypni avatar smitssjors avatar tymek avatar vvo avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

next-connect's Issues

Add Typescript support

I recently started to use next-connect, great work!
The only thing I miss is typescript support.

Add support for base path

When next-connect only supported API Routes, I bet path routing on Next.js routing.

However, when next-connect now being usable in non-api pages (especially document middleware), there may be a need for base path to specify the middleware to only be used in certain places.

Would this be a good thing to have? Or should we be handling path routing ourselve?

No `status` property on ServerResponse

26:14 Property 'status' does not exist on type 'ServerResponse'.
    24 |   res.setHeader('Location', '/');
    25 |
  > 26 |   return res.status(302).end();
       |              ^
    27 | });
    28 |
    29 | export default handler;

Any suggestions?

Pattern match not working

I have created index.js file in api folder and using following code. I am getting 404 on both routes.

It works fine if I use dynamic routing by Next.js. Does this optional pattern really works? Because I want to have single file to handle routes like we do in Express.js instead of creating bunch of nested files.

Getting 404 on these routes

http://localhost:3000/api/user
http://localhost:3000/api/user/555

import nc from 'next-connect';
const handler = nc();

handler.get('/user', (req, res, next) => {
    res.send('User Route');
});

handler.get('/user/:id', (req, res, next) => {
    res.send(`User ${req.query.id} updated`);
});

export default handler;

"API resolved without sending a response" appearing on every route despite the API routes working fine.

I recently switched from an Express server to NextJS's API pages, and I love the way this repo helps with that, so thank you!

That said, all of my API routes are logging "API resolved without sending a response". This issue was posted 3 days ago in nextjs's issues and was closed the next day with this comment being the solution to it. I was wondering if the solution could be implemented into this repo. It looks like the OP was using async/await and the comment that solved the problem recommended returning a Promise instead. In my particular case, all my my routes are printing the message, regardless of if they are asynchronous or not. One example is the following:

import connect from "next-connect"
import common from "../../../middleware/common/"
import keys from "../../../config/keys"

const handler = connect()
handler.use(common)

handler.post((req, res) => {
  return res.status(200).send(keys.googleMapsKey)
})

export default handler

It's possible my problem lies elsewhere, but since all of my routes are going through next-connect, I figured this might be something to be looked at within the repo.

Make Request And Response Extendable in Typescript

I am building an application in typescript using this for routing and middleware. One of my middleware's modifies the request object, adding a method. How would I go about making the method accessible without errors?

Crash when using a a next-connect handler shared between API routes and pages

Hi, I've been using next-connect in a few projects and I encountered a problem, for which I found a workaround while writing the minimal non-working example, but I thought I should still mention it to you because I don't really understand the issue.

The problem arises when I fetch an API from the client side and that I have a shared middleware that I want to apply both in pages and in API routes.

Here's a non-working example: https://gist.github.com/arnaudpeich/cfe6e9379a09f10c9044f9121af8d475

I boiled it down to the minimum that makes the bug occur, but obviously in real projects I use real middleware, not just the base handler.

This first load of the index page works fine, but then when I refresh, I get this res.json is not a function error even on the server side (which seems to indicate that on the reload, the handler for /api/nothing is called even though I'm requesting /index and I'm only calling the API route from the client-side).

It works OK if instead of using the same handler, I create a new handler and use the shared one... but I'm not sure why that is.

Cors in next.js API routes - does not seem to work?

Did anyone get the next-connect package to work with cors package and the relatively new next.js feature - API routes?

pages/api/process file:

import nextConnect from 'next-connect';
import Cors from 'cors'

const apiRoute = nextConnect();

apiRoute
  .use(Cors())

  .post((req, res) => {
    res.status(200).json({ text: 'OK' })
  })

export default apiRoute;

Additional settings in next.config.js:

module.exports = {
  async headers() {
    return [
      {
        source: '/api/process',
        headers: [
          {
            key: 'Access-Control-Allow-Origin',
            value: 'https://external-webpage.com',
          },
          {
            key: 'Vary',
            value: 'Origin',
          },
        ],
      },
    ]
  },
}

I'm getting cors related errors. I might be doing something wrong, if so please correct me!

Meanwhile, when using example from next.js repo, which does not utilize next-connect library, all works just fine:

Any advice will be much appreciated, as I'd rather use next-connect solution which is imo a bit cleaner than the one provided by the next.js team.

MongoError: MongoClient must be connected before calling MongoClient.prototype.db

Hi, I get this error
MongoError: MongoClient must be connected before calling MongoClient.prototype.db

in my middleware database.js
Have this code

export const mongoDb = async () => {
   try {
      if (!global.mongo.client) {
         global.mongo.client = new MongoClient(process.env.MONGODB_URI, {
            useNewUrlParser: true,
            useUnifiedTopology: true,
         });
         await global.mongo.client.connect();
         console.log("Reconnect to db");
      }

      console.log("use Database ");
      return global.mongo.client.db(process.env.DB_NAME);
   } catch (error) {
      if (error) {
         console.error("error 6c1989f1-025d-4ac6-86b3-e75463a9378d", error);
      }
   }
};

I use it in

export async function getStaticProps(context) {
const db = await mongoDb();
const items = await getItems(db, 0, 8);

Everything fine but it crash on heroku, everything ran very well before...
Did I do something wrong?

Cannot read property 'url' of undefined

Hi I got this error. Not sure what is the url ?

error in getStaticProps TypeError: Cannot read property 'url' of undefined
    at Function.handle (D:\Job\anz\node_modules\next-connect\lib\index.js:58:21)
    at D:\Job\anz\node_modules\next-connect\lib\index.js:53:12
    at new Promise (<anonymous>)
    at Function.run (D:\Job\anz\node_modules\next-connect\lib\index.js:52:12)
    at getStaticProps (D:\Job\anz\.next\server\pages\index.js:9301:69)
    at renderToHTML (D:\Job\anz\node_modules\next\dist\next-server\server\render.js:27:115)
    at runMicrotasks (<anonymous>)
    at processTicksAndRejections (internal/process/task_queues.js:97:5)
    at async D:\Job\anz\node_modules\next\dist\next-server\server\next-server.js:98:66
    at async __wrapper (D:\Job\anz\node_modules\next\dist\lib\coalesced-function.js:1:330)

here is my use

await all.run(context.req, context.res);
const items = await getItems(context.req.db, 0, 8);

Add support for catching async handler errors

Currently next-connect has onError option to catch errors thrown by handlers and send relevant response. But it doesn't work with async handlers.
What I would like to have is error thrown from a async handler should get caught in onError without have to write try/catch blocks and then pass the error to next(Error) function.

import nextConnect from 'next-connect';
handler = nextConnect({ onError });

handler.use(async (req, res) => {
  throw new Error("Something failed");
});

Nested routes colliding with one another

Pretty confident this isn't an issue with next-connect, rather something flawed with the approach below, which uses a shared handler across all API routes instead of separately importing next-connect in every route.

The issue
Nested routes are returning the wrong results if you hit one after another. For example, if you go to /api/foo => /api/foo/:id => /api/foo, you will get correct results on the first two calls, but the third will actually be calling the second route and is expecting an id, which throws an error.

I'm running into this on a much larger project, so I pushed up a (very) simplified version of the issue here:
https://github.com/kennethacohen/repro-next-routing-bug

..which is just a modification from something I'm following from @bmvantunes here:
https://github.com/bmvantunes/youtube-2020-july-next-api-routes-next-connect/tree/master/src

Absence of a repository with example

Please make a repo with a working example of your middleware connector.

I have a question - how should Next.js understand to include your handler exported from the file in its life cycle? Should I write something in next.config.js or should I properly name the file where nextConnect() is called... I don't fully understand. Please help me to use your package properly.

How to use async with

Hey can someone show me how to use async with ...

const handler = nc({ attachParams: true });

handler.get("/users/:userId/posts/:postId", (req, res) => {
  // Visiting '/users/12/posts/23' will render '{"userId":"12","postId":"23"}'
  res.send(req.params);
});

I don't seem to be able to make this work. I want to use await when calling Sequelize with the handler.

Rename .apply to something else

handler.apply is a bad idea since it rewrites Function.apply. We need to rename it to something else in a breaking version.

I'm thinking:

  • handler.run
  • handler.runMiddleware

Typescript Issue - NextApiResponse already declared error

I am using the following next.js api code to setup a new api.

import { NextApiRequest, NextApiResponse } from 'next';
import nc from 'next-connect';
import cors from 'cors';

const handler = nc<NextApiRequest, NextApiResponse>();

handler
  .use(cors())
  .get((req: NextApiRequest, res: NextApiResponse) => {
    res.send('GET');
  })
  .post((req: NextApiRequest, res: NextApiResponse) => {
    res.json({ method: 'POST' });
  })
  .put(async (req: NextApiRequest, res: NextApiResponse) => {
    res.end('PUT');
  });

export default handler;

eslint is showing the following error:

Parsing error: Identifier 'NextApiResponse' has already been declared for line 5:

const handler = nc<NextApiRequest, NextApiResponse>();

Although it shows as an error, it does not affect building and running the api - it is just an annoyance right now showing 1 PROBLEM due to this issue. I tried added the no-redeclare rule to my eslintrs.json, but that did not work.

Any insight on how to clear this warning/error would be appreciated.

1.0 Release

This library has been sitting in v0 for awhile. After a period of using it in several production apps, it does not seem to yield any problems. After all, most crux of this library depends on the awesome https://github.com/lukeed/trouter library, so nothing much can really go wrong.

Before the 1.0 release though:

  • I want to make sure the APIs are reasonable so that we can avoid breaking changes to it.
  • Update documentation to be more general. Even though this is originally built of Next.js, I have been using it on top on http very fine. Therefore, it should be about more than Next.js.
  • Decide on whether to add several features like routing caching to improve perf. I have been debating this since one of the goal of this library is to stay lightweight.
  • Decide on whether to add several utils like middleware concurrent execution

Using `.run` in `getServerSideProps` followed be returning a redirect will throw TypeError: argument entity is required

Seems like redirecting by returning { redirect: destination: /, permanent: false} from getServerSideProps after using .run(req, res) to get some session data does not work.

It throws TypeError: argument entity is required with the following stack trace (project specific root has been cleaned from the paths below):

at etag (/web-app-next/node_modules/next/dist/compiled/etag/index.js:1:783)
    at sendPayload (/web-app-next/node_modules/next/dist/next-server/server/send-payload.js:1:567)
    at DevServer.renderToHTMLWithComponents (/web-app-next/node_modules/next/dist/next-server/server/next-server.js:126:518)
    at runMicrotasks (<anonymous>)
    at processTicksAndRejections (internal/process/task_queues.js:94:5)
    at async DevServer.renderToHTML (/web-app-next/node_modules/next/dist/next-server/server/next-server.js:127:711)
    at async DevServer.renderToHTML (/web-app-next/node_modules/next/dist/server/next-dev-server.js:34:578)
    at async DevServer.render (/web-app-next/node_modules/next/dist/next-server/server/next-server.js:70:236)
    at async Object.fn (/web-app-next/node_modules/next/dist/next-server/server/next-server.js:54:264)
    at async Router.execute (/web-app-next/node_modules/next/dist/next-server/server/router.js:24:67)

Running next version 10.0.1 and next-connect 0.9.1.

handlers[(i++)] is not a function

I get the above error because of the testMiddleware. Why is that?

export async function getServerSideProps(ctx) {
const nextConnect = require("next-connect");
const testMiddleware = require("../../db/middlewares/test");
const handler = nextConnect();
handler.use(testMiddleware);

try {
await handler.apply(ctx.req, ctx.res);
} catch (e) {
console.error(e.message);
}
return { props: {} };
}
export default SignIn;

// ../../db/middlewares/test

export default function testMiddleware (req, res, next) {
return next();
}

use with getStaticProps?

hi, next-connect can't use with getStaticProps? I don't think I did something wrong.

export const getStaticProps = async ({req,res}) => {
   const handler= nextConnect()
   handler.use(database)
   await handler.apply(req,res)
   let items = await getProducts(req,0,8);
   return {
      props: { items },
   };
};

image

RequestHandler return type

Documentation states, that get can be called with async function. async always returns a promise. Am I correct to assume, that RequestHandler should have type of void | Promise<void> in this case, or maybe generic <X: any> like Trouter? The same with ErrorHandler.

PR welcome?

Redirects not working?

Hi! I'm still very new to next.js and next-connect, so I'm probably doing something wrong here- but I've been trying to figure this out for the past few hours and I've made no headway.

I'm using next-connect with an openid passport setup. Everything works well except for my /api/return endpoint- after the user logs in on the 3rd-party site and is redirected back to the /api/return endpoint, I want that endpoint to in turn redirect the user to the root of the application (after authenticating using my auth middleware). When I try to redirect to the root of the application though, it appears to hang for a couple minutes before redirecting, without the authentication having gone through and no user/cookie/session being set.

Here's my code in the /api/return endpoint:

handler.use(auth).get(passport.authenticate('steam', { failureRedirect: '/login' }), async (req, res) => {
  res.redirect('/')
})

Replacing res.redirect('/') with res.json({user: req.user}) works perfectly, in terms of authenticating the user and saving it in the session- just doesn't redirect the user back to the root of the application like I want it to.

Again I'm probably doing something dumb, any help would be very much appreciated!

Improve TypeScript interface to catch errors in `NextApiResponse`

When using next-connect with the generics, we don't have any error if we return a wrongly shaped response:

Screenshot 2020-11-14 at 16 37 24

const handler = nc<NextApiRequest, NextApiResponse>()

handler.get<NextApiRequest, NextApiResponse<{ success: boolean }>>(
  async (req, res) => {
    res.json({ success: '42' })
  }
)

However when we declare types directly in the params, we correctly have the error:

Screenshot 2020-11-14 at 16 37 29

const handler = nc<NextApiRequest, NextApiResponse>()

handler.get(
  async (req: NextApiRequest, res: NextApiResponse<{ success: boolean }>) => {
    res.json({ success: '42' })
  }
)

Can we try to improve the typings of the library to get the error? If we write this it works but I'm not sure that's the good way to fix it.

get<T = {}, S = {}>(...handlers: RequestHandler<U | T, V | S>[]): this;

Use global middleware

Is it possible to have global middlewares? If so, what is the recommended way?

This is what I've tried so far.

// pages/api/index.js
import nextConnect from "next-connect";

const handler = nextConnect();

handler.use((req, res, next) => {
  console.log('foo');
  next();
});

export default handler;

And with the base param:

handler.use('/', (req, res, next) => {
  console.log('foo');
  next();
});

But when I access /api/hello, foo is not logged to the console.

// pages/api/hello.js
export default (req, res) => {
  res.statusCode = 200
  res.json({ name: 'John Doe' })
}

Thanks!

Resolver is not a function

I'm getting the following error when attempt to run my next app:
TypeError: resolver is not a function

the file it is erroring in is the following:

pages/api/getMenuItems.js:

import middleware from '../../Middlewares/Middleware'
import nextConnect from 'next-connect';

console.log('in getMenuItems');

const handler = nextConnect();

handler.use(middleware);

console.log('test1');

handler.post(async (req, res) => {
  console.log(await req.db.collection('Pages').findOne());
});

Middlewares/Middleware.js

import nextConnect from 'next-connect';
import dataBuffer from './DatabaseBuffer'

const middleware = nextConnect();

middleware.use(dataBuffer);

export default middleware;

Middlewares/DatabaseBuffer.js

import { MongoClient } from "mongodb";

const client = new MongoClient('mongodb://localhost:27017', {
  useNewUrlParser: true,
  useUnifiedTopology: true
});

export async function setUpDb(db) {
  await db
    .collection('tokens')
    .createIndex('expireAt', { expireAfterSeconds: 0 });
}

export default async function database(req, res, next) {
  if (!client.isConnected()) await client.connect();
  req.dbClient = client;
  req.db = client.db(process.env.DB_NAME);
  await setUpDb(req.db);
  return next();
}

Usage with next-iron-session

I try to use next-connect with next-iron-session but my API call stays in pending.

I don't know if the problem comes from this package or from the other, that's why I opened this issue and another one in next-iron-session.

function withSession(handler) {
  return withIronSession(handler, {
    password: process.env.SECRET_COOKIE_PASSWORD,
    cookieOptions: {
      secure: process.env.NODE_ENV === 'production' ? true : false,
    },
  })
}

const handler = nextConnect()

handler
  .use(withSession)
  .get(async (req, res) => {
    const user = req.session.get('user')
    res.json(user)
  })

export default handler

How to test NextJS endpoints

Looking at your other repo, how would you go about testing an endpoint that uses middleware, or endpoints, in general, using next-connect?

https://github.com/hoangvvo/nextjs-mongodb-app/blob/master/pages/api/users/%5BuserId%5D/index.js#L8

A potential test:

import { createServer } from 'vercel-node-server'
import listen from 'test-listen'
import axios from 'axios'
import handler from '.'

let server
let url

beforeAll(async () => {
  server = createServer(handler)
  url = await listen(server)
})

afterAll(() => {
  server.close()
})

it('should return the expected response', async () => {
  const response = await axios.get(url, { params: { userId: 'SOME_ID' } })
  expect(response.data).toBeTruthy()
})

Doing something like this returns a 500. Maybe it's just me who is lacking testing knowledge. Any help would be appreciated.

Default on error should simply return 500 with a generic error message

It may just be me, but it seems dangerous for the default onError function to potentially expose internals when something blows up within the application.

A saner default behavior should be returning a 500 status code with a generic message like "Internal server error".

People that don't pay too much attention to security can quickly expose sensitive information like this.

Typescript errors when used in /pages

I'm getting the below typescript error when I use handler.apply method inside pages

Argument of type 'IncomingMessage' is not assignable to parameter of type 'NextApiRequest'.
  Type 'IncomingMessage' is missing the following properties from type '{ query: { [key: string]: string | string[]; }; cookies: { [key: string]: string; }; body: any; }': query, cookies, body

So, inside the getServerSideProps/getInitialProps method the req and res we get from context are of type Incoming message and ServerResponse respectively and I'm not able to run the handler because of this issue

Handling errors that are thrown in onNoMatch handler

It's a Feature request.

options.onError function could catch errors that are thrown in options.onNoMatch handler. So in a project we could explicitly throw custom error in onNoMatch function.
As a result, all errors will be processed in Π° single place.

import { NotFoundError } from 'errors/not-found-error';

export const onNoMatch = (): void => {
  throw new NotFoundError();
};

Preloading data automatically

Hi there, I am currently not yet using next-connect. I plan on using it still because its interface seems great.

Before I use it, I need to know how I could solve one pattern: data preloading.

Right now all my Next.js API handlers are wrapped with something like that:

/pages/api/teams/[teamId]/users

function teamUsersApi(req, res, preloadedData) {
  console.log(preloadedData.user);
  console.log(preloadedData.team);
}

export default withPreloadData(teamApi);

When I call /api/teams/200/users then I preload from my database the current user (from reading cookie data) and the team with id 200 (from url params). This is all done by withPreloadData which then calls the API handler with (req, res, preloadedData).

What would be the right pattern to do the same with next-connect? Attaching some property to req maybe? If so what would be the right TypeScript typings to use for that?

Thanks!

console.log doesn't show up in stdout

Hello,

I'm having trouble writing to stdout using console.log in a middleware and couldn't figure it out with any DuckDuckGoing or Googling. Can you help me out? The basic example I have is this:

import nextConnect from "next-connect"
import passport from "passport"
import {Strategy as LocalStrategy} from "passport-local"

let handler = nextConnect()
let passportInit = passport.initialize()
console.log("testing") // this is successfully written to stdout

passport.use(
  new LocalStrategy(function (username, password, done) {
    console.log("the username is", username) // this doesn't get written to stdout
    return done(new Error("err"))
  })
)

After starting the server (running nom run dev), the console.log line under passport.initialize() gets logged to stdout while the console.log("the username is", username) line doesn't. Any idea what I'm doing wrong and how I can get the middleware functions to write to stdout?

Thanks!

[TypeScript] Interface changes in last minor version

Hey,

wanted to ask why the signature changed? Now most of my code is failing because res.json() does not exist on ServerResponse. Given that's still the recommended way of usage in the docs... res.send doesnt exist either.

I can circumvent it with generics, but is this intended?

.get<NextApiRequest & AuthenticatedRequest, NextApiResponse>((req, res) => {
    res.json(req[SESSION_COOKIE_NAME]);
  });

`onError` fails to fire when using custom middleware

When using next-connect with custom or library middleware via the .use method, onError handler does not trigger.

πŸ”Ή Expected

Thrown error caught and handled by next-connect onError method

πŸ”Έ Actual

Thrown error not caught and crashes application

πŸ’» Environment

  • Node: 'v10.16.3'
  • Platform: 'win32'
  • Arch: 'x64'

🌌 Minimal Reproduction Repo

https://github.com/coogie/next-connect-repro/commit/880a4dfc15e84d55ce1b2ede7dbb496a979d45ae

Sup-application should handle sub-routing

Currently, if another instance of next-connect is used in handler.use, the base parameter is ignored.

For example, if an instance of next-connect which has a handler of /bar is used in as handler.use('/foo', thatHandler), the /bar handler should only run in /foo/bar.

Error typescript with the change on #128

Hi,

I just upgrade the next-connect on 0.10.1 and the change :

function NextConnect<U,V>(req: U, res: V): Promise<void>;

into the NextConnect interface make typescript infer any on req & res on .use function.

Example screenshot before / after

before_128

after_128

I don't know enough about TS inference here to make a pull request πŸ˜•.

Pattern match not working

I am trying to integrate next-connect into my project but for some reason I am not getting consistent results while calling the endpoint. Here is the structure of the endpoints I am calling pages->api->category . Within category folder I have following files
index.js - http://localhost:3000/api/category
all.js - http://localhost:3000/api/category/all
[slug].js - http://localhost:3000/api/category/slug

While I am fetching GET- http://localhost:3000/api/category/all
for some reason I am getting GET- http://localhost:3000/api/category/slug
which is the wrong one. Nine times out of ten I am hitting the right endpoint.

Here is the apiRoute

import dbMiddleware from './db';
import nextConnect from 'next-connect';

const apiRoute = nextConnect({
  onError(error, req, res) {
    res
      .status(501)
      .json({ error: `Sorry something Happened! ${error.message}` });
  },
  onNoMatch(req, res) {
    res.status(405).json({ error: `Method '${req.method}' Not Allowed` });
  },
}).use(dbMiddleware);

export default apiRoute;

Cannot find module 'next-connect' in Firebase functions logs

I am in the process of deploying my next.js app to firebase hosting/functions. After a perfectly fine working serve and a error-free deploy, I am seeing the following error in the Firebase functions logs, after calling my api:

{ Error: Cannot find module 'next-connect'
at Function.Module._resolveFilename (internal/modules/cjs/loader.js:636:15)
at Function.Module._load (internal/modules/cjs/loader.js:562:25)
at Module.require (internal/modules/cjs/loader.js:692:17)
at require (internal/modules/cjs/helpers.js:25:18)
at Object.Zb5a (/workspace/dist/app/server/static/iotbXc4GvSgS3_9KfBWUD/pages/api/phrase/[slug].js:200:18)
at webpack_require (/workspace/dist/app/server/static/iotbXc4GvSgS3_9KfBWUD/pages/api/phrase/[slug].js:23:31)
.....

my original src/app/api/phrase/[slug].js looks like this:

import nextConnect from "next-connect";
import middleware from "../../../middleware/database";

const handler = nextConnect();

handler.use(middleware);

handler.get(async (req, res) => {
  const {
    query: { slug },
  } = req;

  const dataModel = {
    _id: String,
    slug: String,
    source: String,
    languages: Array,
  };

  let doc = {};
  if (slug != null) {
    doc = await req.db.collection("phrases").findOne({ slug: slug });
  } else {
    doc = null;
  }
  if (doc != null) {
    res.statusCode = 200;
    res.setHeader("Content-Type", "application/json");

    res.json({ success: true, data: doc });
  } else {
    doc = dataModel;
    res.json({ success: false, data: null });
  }
});

handler.post(async (req, res) => {
  res.json({ message: "post" });
});

handler.put(async (req, res) => {
  res.end({ message: "put" });
});

export default handler;

src/app/middleware/database.js

import { MongoClient } from "mongodb";

const route = ROUTE;
const client = new MongoClient(route, {
  useNewUrlParser: true,
  useUnifiedTopology: true,
});

async function database(req, res, next) {
  if (!client.isConnected()) await client.connect();
  console.log("db connected");
  req.dbClient = client;
  req.db = client.db("public");
  return next();
}

export default database;

package.json

{
  "name": "XXX",
  "version": "0.1.1",
  "private": true,
  "scripts": {
    "dev:client": "next src/app",
    "dev:server": "babel src/server --out-dir dist/server --source-maps --watch",
    "dev": "npm run dev:client & npm run dev:server",
    "build:client": "next build src/app",
    "build:server": "babel src/server --out-dir dist/server --source-maps",
    "build": "npm run build:client && npm run build:server",
    "preserve": "npm run build",
    "serve": "cross-env NODE_ENV=production firebase serve --only functions,hosting",
    "predeploy": "rimraf dist/ && npm run build",
    "deploy": "cross-env NODE_ENV=production firebase deploy --only functions,hosting"
  },
  "main": "dist/server/index.js",
  "engines": {
    "node": "10"
  },
  "dependencies": {
    "@appbaseio/reactivesearch": "^3.8.1",
    "@fortawesome/fontawesome-svg-core": "^1.2.28",
    "@fortawesome/free-solid-svg-icons": "^5.13.0",
    "@fortawesome/react-fontawesome": "^0.1.9",
    "body-parser": "^1.19.0",
    "bootstrap": "^4.4.1",
    "concurrently": "^5.2.0",
    "cors": "^2.8.5",
    "express": "^4.17.1",
    "firebase-admin": "^8.12.1",
    "firebase-functions": "^3.6.2",
    "isomorphic-unfetch": "^3.0.0",
    "mongodb": "^3.5.7",
    "mongoose": "^5.9.14",
    "morgan": "^1.9.1",
    "next": "^9.3.6",
    "next-connect": "^0.7.0",
    "node-fetch": "^2.6.0",
    "react": "16.13.1",
    "react-bootstrap": "^1.0.1",
    "react-cookie-consent": "^3.0.0",
    "react-dom": "16.13.1",
    "react-helmet-async": "^1.0.6"
  },
  "devDependencies": {
    "@babel/cli": "^7.10.1",
    "@babel/core": "^7.10.2"
  }
}

Does someone have an idea, whats wrong?

Possible to have one api file for the whole api?

I was looking into this as a means of cutting down on the lambdas due to vercels new pricing structure.

Is it possible to have a single api file "api/index.ts" that is used to map all the routes?
Or is it restricted to one api per resource user/index.ts, product/index.ts etc?

Thanks

"Object.assign" in add function clear the previous stack middlewares

The function doesn't always work, because stack property is an Array

function add(method, ...handle) {
    for (let i = 0; i < handle.length; i += 1) {
      if (handle[i].stack) Object.assign(this.stack, handle[i].stack);
      else this.stack.push({ handle: handle[i], method });
    }
  }

a temporal solution: Change it with a forEach and push

function add(method, ...handle) {
    for (let i = 0; i < handle.length; i += 1) {
      if (handle[i].stack) handle[i].stack.forEach(objMiddle){this.stack.push(objMiddle);}
      else this.stack.push({ handle: handle[i], method });
    }
  }

Bug: TypeError Trouter is not a constructor

I'm able to get the right responses in the browser:
image

when I navigate to the api /api/coffee.js with the following code:

import nextConnect from "next-connect";

export default nextConnect()
  .get(async (req, res) => {
    res.statusCode = 200;
    res.json({
      "Hello": "World"
    });
  })

However when I go to the home page I get this error:

image

The stack trace points to this line of code: const router = new Trouter();

Any help with this is massively appreciated. Thank you! :)

Add support vs `getInitialProps` or document middleware

Currently, next-connect works with API Routes only.

We need to add support for getInitialProps or better yet document middleware (vercel/next.js#7208). They both share the same PageContext and can be implemented with one same approach.

Current implementation of nextConnect() returns a function with two argument. It is possible to approach it with withMiddleware() or useMiddleware() as in next-session.

possible to use with serverless-http?

Hello, I'm trying to find a lightweight way to use middlewares and method-based dispatch (for REST) with netlify functions, and it seems like next-connect could be promising for this use. Because netlify functions use AWS lamba style handlers with a APIGatewayEvent, Context signature, it seems that we need to use an adapter like serverless-http to make the two work together, similar to how you'd use express in a function context.

However, something doesn't seem to be working, as I am getting this error:

β—ˆ Error during invocation:  {
  errorMessage: 'r is not a constructor',
  errorType: 'TypeError',
  stackTrace: [
    'e.exports (/home/arkadiy/slushie-mvp-api/dist/api/contributor.js:1:3334)',
    'Module.<anonymous> ' +
      '(/home/arkadiy/slushie-mvp-api/dist/api/contributor.js:1:4541)',
    'Module.<anonymous> ' +
      '(/home/arkadiy/slushie-mvp-api/dist/api/contributor.js:1:4645)',
    'n (/home/arkadiy/slushie-mvp-api/dist/api/contributor.js:1:158)',
    '/home/arkadiy/slushie-mvp-api/dist/api/contributor.js:1:957',
    'Object.<anonymous> ' +
      '(/home/arkadiy/slushie-mvp-api/dist/api/contributor.js:1:966)',
    'Module._compile (internal/modules/cjs/loader.js:774:30)',
    'Object.Module._extensions..js (internal/modules/cjs/loader.js:785:10)',
    'Module.load (internal/modules/cjs/loader.js:641:32)',
    'Function.Module._load (internal/modules/cjs/loader.js:556:12)'
  ],

This seems to come up a lot in various framework contexts, but unfortunately I haven't been able to find any post that actually explains what it means.

The code here is just the minimal nc().post(... then wrap handler with serverless

Any idea what could be going awry? One guess is that polymorphic function with 2 args (i.e. without path arg) could be an issue.

Typing incorrect?

Hey, I'm new to typescript, but I'm pretty sure there's something going on with the types exported.

This expression is not callable.
Type 'NextConnect<NextApiRequest, NextApiResponse<any>>' has no call signatures.
ts(2349)

I wrap next-connect for use across a few related projects, and I want to add tests for the interface. So I'm manually calling the resulting api handler. A simplified case:

const getRouter = () => router(routeOpts);
const handler = getRouter().get((req, res) => res.send('ok'));

await handler({method: 'GET'}, mockRes); // TS error above

I believe the fix in index.d.ts is on lines ~26-28. Move the function declaration inside the interface:

  interface NextConnect<U, V> {
    (req: U, res: V): Promise<void>;

    use<T = {}, S = {}>(...handlers: Middleware<U & T, V & S>[]): this;
    // all the rest...

That said, I'm not totally sure. Like I said, I'm still learning the ropes with TS.

500 error in testing with supertest/Node http server

Possibly a duplicate of #84.

Here's a CodeSandbox showing the issueβ€”open up a new terminal in the bottom right corner and run yarn test to see the result.

Tests:

const { createServer } = require('http');
const request = require('supertest');
const nc = require('next-connect');
const postDocument = require('../../utils/dbUtil');

describe('document POST endpoint', () => {
  const url = '/';
  const handler = nc().post(url, postDocument);

  it('results in 404 on get', async () => {
    const app = createServer(handler);
    const result = await request(app).get(url);
    expect(result.status).toBe(404);
  });

  it('results in 200 on post', async () => {
    const app = createServer(handler);
    const result = await request(app).post(url).send({
      text: 'test',
    });
    expect(result.status).toBe(200);
  });
});

The required ../../utils/dbUtil:

const postDocument = async (req, res) => {
  res.status(200).json(req);
};
export default postDocument;

Result:

 FAIL  src/__tests__/api/document.test.js
  document POST endpoint
    βœ“ results in 404 on get (15ms)
    βœ• results in 200 on post (5ms)

  ● document POST endpoint β€Ί results in 200 on post

    expect(received).toBe(expected) // Object.is equality

    Expected: 200
    Received: 500

      19 |       text: 'test',
      20 |     });
    > 21 |     expect(result.status).toBe(200);
         |                           ^
      22 |   });
      23 | });
      24 | 

      at Object.<anonymous> (src/__tests__/api/document.test.js:21:27)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 passed, 2 total
Snapshots:   0 total
Time:        2.845s, estimated 3s

Am I doing something wrong here? Maybe it's something with the way supertest makes requests?

Types for next-connect

I cannot seem to find the type definitions for next-connect - please advise if there is an npm package.

Express Router support

I realize this is a very specific ask, but I'm trying to get background queues working with NextJs and the final piece is getting some visibility into the queues withbull-board.

In theory, you'd think you could pull this off:

// pages/api/queues.js
import nc from "next-connect";
const { setQueues, BullAdapter, router, UI } = require("bull-board");
import emailQueue from 'queues/emailQueue';


setQueues([
  new BullAdapter(emailQueue),
]);

const handler = nc()
  .use((req, res, next) => {
    // process.env.HOST === 'domain.com/api'
    req.proxyUrl = process.env.HOST + "/queues";
    next();
  })
  .use(router);

export default handler;

But it's just rendering JSON instead of the web UI. Maybe it's just some incompatibility with express type of middleware?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.