Gathering detailed insights and metrics for @arrirpc/server
Gathering detailed insights and metrics for @arrirpc/server
Gathering detailed insights and metrics for @arrirpc/server
Gathering detailed insights and metrics for @arrirpc/server
Arri RPC is a code-first RPC framework for end-to-end type safety in any language
npm install @arrirpc/server
Typescript
Module System
Node Version
NPM Version
TypeScript (40.31%)
Rust (15.1%)
Kotlin (14.69%)
Swift (14.02%)
Dart (11.51%)
Go (4.27%)
JavaScript (0.1%)
Shell (0.01%)
Total Downloads
5,463
Last Day
3
Last Week
139
Last Month
582
Last Year
5,463
69 Stars
1,105 Commits
3 Forks
1 Watching
5 Branches
3 Contributors
Minified
Minified + Gzipped
Latest Version
0.74.0
Package Id
@arrirpc/server@0.74.0
Unpacked Size
109.66 kB
Size
22.57 kB
File Count
8
NPM Version
10.9.0
Node Version
22.12.0
Publised On
30 Jan 2025
Cumulative downloads
Total Downloads
Last day
-86.4%
3
Compared to previous day
Last week
13%
139
Compared to previous week
Last month
4.1%
582
Compared to previous month
Last year
0%
5,463
Compared to previous year
9
1
Typescript implementation of Arri RPC. It's built on top of H3 and uses esbuild for bundling.
Parameters and responses are defined using @arrirpc/schema for automatic validation and serialization of inputs and outputs and to generate Arri Type Definitions for client generators.
1# npm 2npx arri init [project-name] 3cd [project-name] 4npm install 5npm run dev 6 7# pnpm 8pnpm dlx arri init [project-name] 9cd [project-name] 10pnpm install 11pnpm run dev
1# npm 2npm install -D arri 3npm install @arrirpc/server @arrirpc/schema 4 5# pnpm 6pnpm install -D arri 7pnpm install @arrirpc/server @arrirpc/schema
A basic Arri app directory looks something like this:
1|-- project-dir 2 |-- .arri // temp files go here 3 |-- .output // final bundle goes here 4 |-- src 5 |-- procedures // .rpc.ts files go here 6 |-- app.ts 7 |-- arri.config.ts 8 |-- package.json 9 |-- tsconfig.json 10|
Both .arri
and .output
should be added to your .gitignore
file
1.arri 2.output 3node_modules
Create an arri.config.ts
in the project directory
1// arri.config.ts 2import { defineConfig, servers, generators } from 'arri'; 3 4export default defineConfig({ 5 server: servers.tsServer(), 6 generators: [ 7 generators.typescriptClient({ 8 // options 9 }), 10 generators.dartClient({ 11 // options 12 }), 13 ], 14});
Create an app entry file in your src directory. The name of the file must match whatever you put as the entry
in your arri.config.ts
.
1// ./src/app.ts 2import { ArriApp } from 'arri'; 3 4const app = new ArriApp(); 5 6export default app;
Setup your npm scripts:
1{ 2 "name": "my-arri-app", 3 "type": "module", 4 "scripts": { 5 "dev": "arri dev", 6 "build": "arri build" 7 }, 8 "dependencies": { 9 ... 10 }, 11 "devDependencies": { 12 ... 13 } 14}
Arri RPC comes with an optional file based router that will automatically register functions in the ./procedures
directory that end with the .rpc.ts
file extension.
1|-- src 2 |-- procedures 3 |-- sayHello.rpc.ts // becomes sayHello() 4 |-- users 5 |-- getUser.rpc.ts // becomes users.getUser() 6 |-- updateUser.rpc.ts // becomes users.updateUser()
Example .rpc.ts
file
1// ./src/users/getUser.rpc.ts 2import { defineRpc } from '@arrirpc/server'; 3import { a } from '@arrirpc/schema'; 4 5export default defineRpc({ 6 params: a.object({ 7 userId: a.string(), 8 }), 9 response: a.object({ 10 id: a.string(), 11 name: a.string(), 12 createdAt: a.timestamp(), 13 }), 14 handler({ params }) { 15 // function body 16 }, 17});
1export default defineConfig({
2 servers: servers.tsServer({
3 procedureDir: 'procedures', // change which directory to look for procedures (This is relative to the srcDir)
4 procedureGlobPatterns: ['**/*.rpc.ts'], // change the file name glob pattern for finding rpcs
5 }),
6 // rest of config
7});
For those that want to opt out of the file-based routing system you can manually register procedures like so.
1// using the app instance 2const app = new ArriApp() 3app.rpc('sayHello', 4 defineRpc({...}) 5); 6 7// defining a service 8const app = new ArriApp(); 9const usersService = defineService("users"); 10usersService.rpc("getUser", defineRpc({...})); 11usersService.rpc("createUser", defineRpc({...})); 12 13// register the service on the app instance 14app.use(usersService);
There's also a shorthand for initializing services with procedures
1// this is equivalent to what we showed above 2const usersService = defineService("users", { 3 getUser: defineRpc({..}), 4 createUser: defineRpc({..}), 5});
Event stream procedures make use of Server Sent Events to stream events to clients.
Arri Event streams sent the following event types:
message
- A standard message with the response data serialized as JSONdone
- A message to tell clients that there will be no more eventsping
- A message periodically sent by the server to keep the connection alive.1/// message event /// 2id: string | undefined; 3event: 'message'; 4data: Response; // whatever you have specified as the response serialized to json 5 6/// done event /// 7event: 'done'; 8data: 'this stream has ended'; 9 10/// ping event /// 11event: 'ping'; 12data: '';
1// procedures/users/watchUser.rpc.ts 2export default defineEventStreamRpc({ 3 params: a.object({ 4 userId: a.string(), 5 }), 6 response: a.object({ 7 id: a.string(), 8 name: a.string(), 9 createdAt: a.timestamp(), 10 updatedAt: a.timestamp(), 11 }), 12 handler({ params, stream }) { 13 // initialize the stream and send it to the client 14 stream.send(); 15 16 // send a message every second 17 const interval = setInterval(async () => { 18 await stream.push({ 19 id: '1', 20 name: 'John Doe', 21 createdAt: new Date(), 22 updatedAt: new Date(), 23 }); 24 }, 1000); 25 26 // cleanup when the client disconnects 27 stream.on('close', () => { 28 clearInterval(interval); 29 }); 30 }, 31});
1// send the stream to the client. Must be called before pushing any messages 2stream.send() 3// push a new message to the client 4stream.push(data: Data, eventId?: string) 5// close the stream and tell the client that there will be no more messages 6stream.close() 7// register a callback that will fire after the stream has been close by the server or the connection has been dropped 8stream.onClosed(cb: () => any)
You can also add generic endpoints for instances when a message-based RPC endpoint doesn't fit.
1// using the app instance 2const app = new ArriApp(); 3app.route({ 4 method: "get", 5 path: "/hello-world", 6 handler(event) { 7 return "hello world"; 8 }, 9}); 10 11// using a sub-router 12const app = new ArriApp(); 13const router = new ArriRouter(); 14router.route({ 15 method: "get", 16 path: "/hello-world", 17 handler(event) { 18 return "hello world", 19 } 20}) 21app.use(router) 22 23// sup-routers can also specify a route prefix 24const router = new ArriRouter("/v1") 25router.route({ 26 method: "get", 27 path: "/hello-world", // this will become /v1/hello-world 28 handler(event) { 29 return "hello world" 30 } 31});
1const app = new ArriApp(); 2 3const requestLoggerMiddleware = defineMiddleware((event) => { 4 console.log(`new request at ${event.path}`); 5}); 6 7app.use(requestLoggerMiddleware);
Any values added to event.context
will become available in the rpc instance
1const authMiddleware = defineMiddleware(async (event) => { 2 // assume you did something to get the user from the request 3 event.context.user = { 4 id: 1, 5 name: 'John Doe', 6 email: 'johndoe@gmail.com', 7 }; 8}); 9 10app.rpc('sayHello', { 11 params: undefined, 12 response: a.object({ 13 message: a.string(), 14 }), 15 // user is available here 16 handler({ user }) { 17 return { 18 message: `Hello ${user.name}`, 19 }; 20 }, 21});
To get type safety for these new properties create a .d.ts
file and augment the ArriEventContext
provided by @arrirpc/server
1import '@arrirpc/server'; 2 3declare module '@arrirpc/server' { 4 interface ArriEventContext { 5 user?: { 6 id: number; 7 name: string; 8 email: string; 9 }; 10 } 11}
1// arri.config.ts 2import { defineConfig, servers, generators } from "arri"; 3 4export default defineConfig({ 5 server: servers.tsServer(), 6 generators: [ 7 generators.typescriptClient({...}), 8 generators.dartClient({...}), 9 generators.kotlinClient({...}) 10 generators.someGenerator({...}) 11 ] 12});
For info on what generators are available see here
For info on how to create your own generator see @arrirpc/codegen-utils
The server generates a __definition.json
file that acts as a schema for all of the procedures and models in the API. By default this schema is viewable from /__definition
when the server is running, but it can be modified. The endpoint is also relative to the rpcRoutePrefix
option.
It looks something like this:
1{ 2 "procedures": { 3 "sayHello": { 4 "transport": "http", 5 "path": "/say-hello", 6 "method": "post", 7 "params": "SayHelloParams", 8 "response": "SayHelloResponse" 9 } 10 // rest of procedures 11 }, 12 "definitions": { 13 "SayHelloParams": { 14 "properties": { 15 "name": { 16 "type": "string" 17 } 18 } 19 }, 20 "SayHelloResponse": { 21 "properties": { 22 "message": { 23 "type": "string" 24 } 25 } 26 } 27 // rest of models 28 } 29}
Arri is able to use this schema file to automatically generate clients in multiple languages. In this way it works similarly to an Open API schema, but with much better code-generation support. I've made a lot of deliberate choices in designing this schema to make code-generation easier and more consistent across languages. For example, Arri schemas use a superset of JSON Type Definition for their models instead of JSON Schema.
Every procedure maps to a different url based on it's name. For example given the following file structure:
1|--src 2 |--procedures 3 |--getStatus.rpc.ts 4 |--users 5 |--getUser.rpc.ts 6 |--updateUser.rpc.ts
We will get the following endpoints:
1POST /get-status 2POST /users/get-user 3POST /users/update-user 4 5(Note: these will always be relative to the `rpcRoutePrefix` option)
By default all procedures will become post requests, but you can change this when creating a procedure:
1// procedures/users/getUser.rpc.ts 2export default defineRpc({ 3 method: 'get', 4 // rest of config 5});
The supported HTTP methods are as follows:
When using a get method the RPC params will be mapped as query parameters which will be coerced into their type using the a.coerce
method from arri-validate
. Get methods support all basic scalar types however arrays and nested objects are not supported.
Arri is built on top of H3 so many of the concepts that apply to H3 also apply to Arri.
Arri re-eports all of the H3 utilities.
1import { getRequestIP, setResponseHeader } from '@arrirpc/server';
You can access H3 events from inside procedures handlers.
1defineRpc({
2 params: undefined,
3 response: undefined,
4 handler(_, event) {
5 getRequestIP(event);
6 },
7});
8
9defineEventStreamRpc({
10 params: undefined,
11 response: undefined,
12 handler(_, event) {
13 getRequestIP(event);
14 },
15});
Arri server is just an H3 app under the hood so you can start it the same way you would start an H3 app. Although you should note that currently the filed based router only works when using the Arri CLI.
1import { createServer } from 'node:http'; 2import { ArriApp, toNodeListener } from '@arrirpc/server'; 3 4const app = new ArriApp(); 5 6createServer(toNodeListener(app.h3App)).listen(process.env.PORT || 3000);
1# start the dev server 2arri dev [flags] 3 4# create a production build 5arri build [flags] 6 7# create a new project 8arri init [dir] 9 10# run codegen 11arri codegen [path-to-definition-file]
Run nx build arri-rpc
to build the library.
Run nx test arri-rpc
to execute the unit tests via Vitest.
No vulnerabilities found.
No security vulnerabilities found.