remix-api-router
v1.2.0
Published
Library for implementing APIs in Remix using a chaining approach, similar to Express middleware
Downloads
9
Maintainers
Readme
Remix API Router
Library for creating APIs in Remix using a chaining approach, similar to Express middleware.
Note: originally I was planning to implement a chain-of-responsibility pattern with the same contract as Express, but I wasn't sure how much adoption it will have. If you are interesting in it, let's talk!.
Installation
npm i remix-api-router
or
yarn add remix-api-router
Usage
For example, create a products.tsx file inside a Remix app, under the app/routes/api folder, and paste the following code:
import { apiRouter } from "remix-api-router";
import { checkAuth } from "auth";
import { json } from "@remix-run/node";
import type { ActionFunction, LoaderFunction, DataFunctionArgs } from "@remix-run/node";
/**
* /api/products
*/
// Define all routes for this endpoint and their handlers
const router = apiRouter();
router
.get(checkAuth, async (args: DataFunctionArgs) => {
await fetch("https://google.com");
return json({ message: "GET" }, 200);
})
.post(checkAuth, (args: DataFunctionArgs) => json({ message: "POST" }, 200))
.put((args: DataFunctionArgs) => json({ message: "PUT" }, 200))
.patch((args: DataFunctionArgs) => json({ message: "PATCH" }, 200))
.delete((args: DataFunctionArgs) => {
throw new Error("unexpected error");
})
.error((err) => {
return json({ error: "Custom message: Server unavailable!" }, 500);
});
export const loader: LoaderFunction = router.loader();
export const action: ActionFunction = router.actions();
Note see a fully working example here.
Features
- Same API as Remix for developing handlers: it's just sugar syntax to provide a chaining-like approach.
- Provides a handler for errors/exceptions of your code by default, returning
500
(you can provide your own). - Provides a handler for no matching routes, returning
405
(you can provide your own). - Lightweight (~ 4.5KB) => Suitable for serverless environment.
- TypeScript support.
API
General rules
- Create a single router instance per file.
- A handler that returns a value stops the chaining process. The expected return value is a
Response
object, just what Remix expects. - A handler that returns either nothing or a Promise with no return value (
void
orPromise<void>
will tell the chain to continue processing the request with the other handlers. - A handler that throws an error will also tell the chain to stop processing the request. You can configure the error handling with your own logic by configuring the
router.error
handler. - If a request arrives and no handler is configured, it will return a default response:
405 Method not allowed
. You can configure this with your own logic by configuring therouter.noMatch
handler. - Always connect the router with Redux via the named exports
loader
andaction
. - Configure handlers once per router, as other configurations will override the previous ones.
Configuration
You can initialize the router using either a class instantiation or a factory instantiation:
Use the default export for a class instantiation:
import ApiRouter from "remix-api-router"; import { json } from "@remix-run/node"; const router = new ApiRouter(); router.get(() => json({ hello: "world" }, 200)).post(() => json({ hello: "world" }, 201));
Use the named export "apiRouter" for the factory instantiation:
import { apiRouter } from "remix-api-router"; import { json } from "@remix-run/node"; const router = new ApiRouter(); router.get(() => json({ hello: "world" }, 200)).post(() => json({ hello: "world" }, 201));
Last, hook the router with the Remix loader
and action
by simply exporting the following at the end of the file:
import { apiRouter } from "remix-api-router";
import { json } from "@remix-run/node";
const router = apiRouter();
router
.get(() => json({ hello: "world" }, 200))
.post(() => json({ hello: "world" }, 201))
...
export const loader: LoaderFunction = router.loader();
export const action: ActionFunction = router.actions();
Route handling
It supports all HTTP Methods by adding a handler with the same contract provided by Remix. For example:
To handle GET /api/categories/$id
requests, use the following code:
import { apiRouter } from "remix-api-router";
import { json } from "@remix-run/node";
import type { ActionFunction, LoaderFunction, DataFunctionArgs } from "@remix-run/node";
/**
* /api/categories/$id
*/
const router = apiRouter();
router.get(({ request, context, params }: DataFunctionArgs) => {
return json({ message: `Requested category with ID ${params.id}` }, 200);
});
export const loader: LoaderFunction = router.loader();
export const action: ActionFunction = router.actions();
To handle GET /api/categories
and POST /api/categories
requests, use:
import { apiRouter } from "remix-api-router";
import { json } from "@remix-run/node";
import type { ActionFunction, LoaderFunction, DataFunctionArgs } from "@remix-run/node";
/**
* /api/categories
*/
const router = apiRouter();
router
.get(async ({ request, context, params }: DataFunctionArgs) => {
// See https://remix.run/docs/en/v1/guides/data-loading#url-search-params
const url = new URL(request.url);
const term = url.searchParams.get("term");
return json(await fakeProductSearch(term));
})
.post(async ({ request, context, params }: DataFunctionArgs) => {
const body = await request.json();
return json({ message: `Sent body ${JSON.stringify(body)}` });
});
async function fakeProductSearch(term: string | null) {
return [
{ id: 1, name: "Category A" },
{ id: 2, name: "Category B" },
];
}
export const loader: LoaderFunction = router.loader();
export const action: ActionFunction = router.actions();