next-rpc
next-rpc
makes exported functions from API routes accessible in the browser. Just import your API function and call it anywhere you want.
Example
Define your rpc route as follows:
// /pages/api/countries.js
export const config = { rpc: true }; // enable rpc on this API route
// export a function that needs to be called from the server and the browser
export async function getName(code) {
return db.query(`SELECT name FROM country WHERE code = ?`, code);
}
Now in your components you can just import getName
and call it anywhere you want:
// /pages/index.js
import { getName } from './api/countries';
export default function MyPage({ initialData }) {
const [countryName, setCountryName] = React.useState(initialData);
return (
<button onClick={() => getName('BE').then(setCountryName)}>
{countryName || 'click me'}
</button>
);
}
Installation
Install the next-rpc
module
npm install -S next-rpc
configure Next.js to use the module
// ./next.config.js
const withRpc = require('next-rpc')();
module.exports = withRpc({
// your next.js config goes here
});
Why this library is needed
Next.js 9.3 introduced getServerSideProps
and getStaticProps
. New ways of calling serverside code and transfer the data to the browser. a pattern emerged for sharing API routes serverside and browserside. In short the idea is to abstract the logic into an exported function for serverside code and expose the function to the browser through an API handler.
// /pages/api/myApi.js
export async function getName(code) {
return db.query(`SELECT name FROM country WHERE code = ?`, code);
}
export default async (req, res) => {
res.send(await getName(req.query.code));
};
This pattern is great as it avoids hitting the network when used serverside. Unfortunately, to use it client side it still involves a lot of ceremony. i.e. a http request handler needs to be set up, fetch
needs to be used in the browser, the input and output needs to be correctly encoded and decoded. Error handling needs to be set up to deal with network related errors. If you use typescript you need to find a way to propagate the types from API to fetch result. etc...
Wouldn't it be nice if all of that was automatically handled and all you'd need to do is import getName
on the browserside, just like you do serverside? That's where next-rpc
comes in. With a next-rpc
enabled API route, all its exported functions automatically become available to the browser as well.
Note:
next-rpc
is not meant as a full replacement for Next.js API routes. Some use cases are still better solved with classic API routes. For instance when you want to rely on the existing browser caching mechanisms.
Rules and limitations
-
Rpc routes are only allowed to export async functions. They also need to be statically analyzable as such. Therefore only the following is allowed, either:
export async function fn1() {} export const fn2 = async () => {};
-
All inputs and outputs must be simple JSON serializable values.
-
a default export is not allowed.
next-rpc
will generate one. -
You must enable rpc routes through the
config
export. It must be an exported object that has therpc: true
property.
typescript
Try the example on codesandbox
next-rpc
works really nicely with typescript. There is no serialization layer so functions just retain their type sigantures both on server and client.
swr
Try the example on codesandbox
next-rpc
can work seamlessly with swr
.
// ./pages/api/projects.js
export const config = { rpc: true };
export async function getMovies(genre) {
return db.query(`...`);
}
// ./pages/index.jsx
import useSwr from 'swr';
import { getMovies } from './api/movies';
import MoviesList from '../components/MoviesList';
const callFn = (method, ...params) => method(...params);
export default function Comedies() {
const { data, error } = useSwr([getMovies, 'comedy'], callFn);
if (error) return <div>failed to load</div>;
if (!data) return <div>loading...</div>;
return <MoviesList items={data} />;
}
react-query
Try the example on codesandbox
next-rpc
can also work with react-query
.
// ./pages/api/projects.js
export const config = { rpc: true };
export async function getMovies(genre) {
return db.query(`...`);
}
// ./pages/index.jsx
import { QueryClient, QueryClientProvider, useQuery } from 'react-query';
import { getMovies } from './api/movies';
import MoviesList from '../components/MoviesList';
function App() {
const queryClient = React.useMemo(() => new QueryClient(), []);
return (
<QueryClientProvider client={queryClient}>
<Movies genre="comedy" />
</QueryClientProvider>
);
}
export default function Movies({ genre = 'comedy' }) {
const { isLoading, error, data } = useQuery(['getMovies', genre], () =>
getMovies(genre)
);
if (error) return <div>failed to load</div>;
if (isLoading) return <div>loading...</div>;
return <MoviesList items={data} />;
}
Middleware
next-rpc
allows for defining middleware functions that automatically wrap all your API methods. this could be useful for logging purposes. To define such middleware you can supply a wrapMethod
option to the config
export. This function receives the method it's wrapping along with some metadata and is expected to return a function with the exact same signature. Example:
import { NextRpcConfig, WrapMethod } from 'next-rpc';
const wrapMethod: WrapMethod = (method, meta) => {
return async (...args) => {
console.log(`calling "${meta.name}" on "${meta.pathname}" with ${args}`);
const result = await method(...args);
console.log(`result: ${result}`);
return result;
};
};
export const config: NextRpcConfig = {
rpc: true,
wrapMethod,
};
next-rpc
Debugging Since version 3.7.0, next-rpc
uses the JSON-RPC format for its messages. This makes it possible to use tools like JSON RPC Chrome Viewer to introspect the rpc frames that are being transferred by next-rpc
.
Disclaimer: I have no affiliation with this extension, use at your own discretion.
Next.js request context
β οΈ warning:request context is not supported in the Next.js edge runtime and won't be for the foreseeable future.
This library completely hides the network layer. This makes it elegant to use, but also imposes limitations. To efficiently be able to implement things like cookie authentication, access to the underlying requests is required. To enable that, this library introduces next-rpc/context
. An example:
// ./pages/api/myRpc.js
import { getContext } from 'next-rpc/context';
const config = { rpc: true };
export async function currentUser() {
const { req } = getContext();
return getUserFromRequest(req);
}
The req
variable in the previous example will contain the IncomingMessage
that lead to the call of currentUser()
. That means it will receive req
from either:
NextPageContext
: if it traces back togetServerSideProps
orgetInitialProps
.NextApiHandler
: if it traces back to a call in another API handler, or if it was called from the browser.
next-rpc
intercepts all instances of getInitialProps
, getServerSideProps
and api handlers and injects its context provider in there. From there on, every function invocation that descends from that point will be able to access the context through getContext
. Since this feature relies on experimental APIs, it needs to be explicitly enabled by configuring the experimentalContext
flag in next.config.js
:
// ./next.config.js
const withRpc = require('next-rpc')({
experimentalContext: true,
});
module.exports = withRpc();
How it works
next-rpc
compiles api routes. If it finds rpc
enabled it will rewrite the module. In serverside bundles, it will generate an API handler that encapsulates all exported functions. For browserside bundles, it will replace each exported function with a function that uses fetch
to call this API handler.
It's important to note that next-rpc
intends to be fully backwards compatible. If you don't specify the rpc
option, the API route will behave as it does by default in Next.js.
Roadmap
- Improve dev experience: warn when using unserializable input/output
- Custom contexts: it should be possible to build on the context feature to provide a custom context. e.g.
UserContext
.
Contributing
Bug fixing PRs are welcome, provided they are of high quality and accompagnied by tests. Don't open PRs for feature requests prior to my approval.