3.0.8 • Published 18 days ago

@visulima/connect v3.0.8

Weekly downloads
-
License
MIT
Repository
github
Last release
18 days ago

http-errors, regexparam

and based on

next-connect

typescript-image npm-image license-image



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 @visulima/connect
yarn add @visulima/connect
pnpm add @visulima/connect

Usage

Note

Although @visulima/connect is initially written for Next.js, it can be used in http server, Vercel. See Examples for more integrations.

Below are use cases.

Next.js API Routes

// pages/api/hello.js
import type { NextApiRequest, NextApiResponse } from "next";
import { createNodeRouter, expressWrapper } from "@visulima/connect";
import cors from "cors";

// Default Req and Res are IncomingMessage and ServerResponse
// You may want to pass in NextApiRequest and NextApiResponse
const router = createNodeRouter<NextApiRequest, NextApiResponse>({
    onError: (err, req, res) => {
        console.error(err.stack);
        res.status(500).end("Something broke!");
    },
    onNoMatch: (req, res) => {
        res.status(404).end("Page is not found");
    },
});

router
    .use(expressWrapper(cors())) // express middleware are supported if you wrap it with expressWrapper
    .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) => {
        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 createNodeRouter 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 });
        },
    );

export default router.handler();

Next.js getServerSideProps

// page/users/[id].js
import { createNodeRouter } from "@visulima/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 = createNodeRouter()
    .use(async (req, res, next) => {
        // this serve as the error handling middleware
        try {
            return await next();
        } catch (e) {
            return {
                props: { error: e.message },
            };
        }
    })
    .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 } };
    })
    .post(async (req, res) => {
        const user = await updateUser(req);
        return { props: { user, updated: true } };
    });

export async function getServerSideProps({ req, res }) {
    return router.run(req, res);
}

Next.js Edge API Routes (Beta)

Edge Router can be used in Edge Runtime

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

// Default Req and Evt are Request and unknown
// You may want to pass in NextRequest and NextFetchEvent
const router = createEdgeRouter<NextRequest, NextFetchEvent>({
    onError: (err, req, evt) => {
        console.error(err.stack);
        return new NextResponse("Something broke!", {
            status: 500,
        });
    },
    onNoMatch: (req, res) => {
        return new NextResponse("Page is not found", {
            status: 404,
        });
    },
});

router
    .use(expressWrapper(cors())) // express middleware are supported if you wrap it with expressWrapper
    .use(async (req, evt, 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) => {
        return new Response("Hello world");
    })
    .post(async (req, res) => {
        // use async/await
        const user = await insertUser(req.body.user);
        res.json({ user });
        return new Response(JSON.stringify({ user }), {
            status: 200,
            headerList: {
                "content-type": "application/json",
            },
        });
    })
    .put(async (req, res) => {
        const user = await updateUser(req.body.user);
        return new Response(JSON.stringify({ user }), {
            status: 200,
            headerList: {
                "content-type": "application/json",
            },
        });
    });

export default router.handler();

Next.js Middleware

Edge Router can be used in Next.js Middleware

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

// This function can be marked `async` if using `await` inside

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

router.use(async (request, _, next) => {
    await logRequest(request);
    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((request) => {
    // default if none of the above matches
    return NextResponse.next();
});

export function middleware(request: NextRequest) {
    return NextResponse.redirect(new URL("/about-2", request.url));
}

API

The following APIs are rewritten in terms of NodeRouter (createNodeRouter), but they apply to EdgeRouter (createEdgeRouter) as well.

router = createNodeRouter()

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 nodeHandler has run
});

// Or include a base
router2.use("/foo", fn); // Only run in /foo/**

// mount an instance of router
const sub1 = createNodeRouter().use(fn1, fn2);
const sub2 = createNodeRouter().use("/dashboard", auth);
const sub3 = createNodeRouter().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", nodeHandler) inside page/api/index.js does not serve that nodeHandler at /api/foo.

router.all(pattern, ...fns)

Same as .METHOD but accepts any methods.

router.handler(options)

Create a nodeHandler to handle incoming requests.

options.onError

Accepts a function as a catch-all error nodeHandler; executed whenever a nodeHandler 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");
}

const router = createNodeRouter({ onError });

export default router.handler();

options.onNoMatch

Accepts a function of (req, res) as a nodeHandler 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!?");
}

const router = createNodeRouter({ onNoMatch });

export default router.handler();

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(thisMiddlewarewareMightThrow);

await router.run(req, res);

Common errors

There are pitfalls in using @visulima/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 nodeHandler would resolve before all the code in each layer runs.

const nodeHandler = 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 nodeHandler(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 createNodeRouter().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 createNodeRouter().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 nodeHandler = createNodeRouter()
    .use((req, res) => {
        // BAD: res.redirect is not a function (not defined in `getServerSideProps`)
        // See https://github.com/hoangvvo/@visulima/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 nodeHandler() directly in getServerSideProps.
// page/index.js
const router = createNodeRouter().use(foo).use(bar);
const nodeHandler = router.handler();

export async function getServerSideProps({ req, res }) {
    await nodeHandler(req, res); // BAD: You should call router.run(req, res);
    return {
        props: {},
    };
}

Recipes

Next.js

If you created the file /api/<specific route>.js folder, the nodeHandler 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 { createNodeRouter } from "@visulima/connect";

const router = createNodeRouter()
    .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 separating routes into different files (/api/user/[userId].js, /api/hello.js) in the future.

Express.js Compatibility

Express middleware is not built around promises but callbacks. This prevents it from playing well in the @visulima/connect model. Understanding the way express middleware works, we can build a wrapper like the below:

import { expressWrapper } from "@visulima/connect";
import someExpressMiddleware from "some-express-middleware";

router.use(expressWrapper(someExpressMiddleware));

Supported Node.js Versions

Libraries in this ecosystem make the best effort to track Node.js’ release schedule. Here’s a post on why we think this is important.

Contributing

If you would like to help take a look at the list of issues and check our Contributing guild.

Note: please note that this project is released with a Contributor Code of Conduct. By participating in this project you agree to abide by its terms.

Credits

License

The visulima connect is open-sourced software licensed under the MIT

3.0.8

18 days ago

3.0.7

26 days ago

3.0.6

1 month ago

3.0.5

1 month ago

3.0.4

1 month ago

3.0.3

2 months ago

3.0.2

2 months ago

3.0.1

2 months ago

3.0.0

2 months ago

2.1.14

2 months ago

2.1.13

3 months ago

2.1.12

4 months ago

2.1.11

5 months ago

1.3.7

9 months ago

2.1.2

7 months ago

2.1.1

7 months ago

2.1.4

6 months ago

2.1.3

7 months ago

2.1.6

6 months ago

2.1.5

6 months ago

2.1.8

6 months ago

2.1.7

6 months ago

2.1.0

8 months ago

2.0.1

8 months ago

2.0.0

8 months ago

2.1.9

5 months ago

2.1.10

5 months ago

1.3.8

9 months ago

1.3.6

11 months ago

1.3.5

11 months ago

1.3.4

12 months ago

1.3.3

1 year ago

1.3.2

1 year ago

1.3.1

1 year ago

1.3.0

1 year ago

1.2.0

1 year ago

1.1.1

1 year ago

1.1.0

1 year ago

1.0.1

2 years ago

1.0.0

2 years ago