slim-rpc v0.0.8
Slim-RPC
- Trpc inspired RPC lib
- Expects Zod.scheme as input validation
- e2e intellisense
under 300 lines of code so you can easily extend it
Motivation
Trpc is a really great library! I wanted to see if I can develop it's core feature and DX my self and was surprised by the short amount of code it took and how well it worked for me So I decided to share it with the world.
Server
Augmenting the RpcContext
the RpcContext is an object that is being created for each RPC function handler invocation. In order to have intellisense available for each RPC function you will need to use Typescript augmentation for that in your server project root create a d.ts file and add inside RpcContext whatever you want
import { RpcContext } from "slim-rpc/lib/cjs/models";
import { UserService } from "./users/user.service";
import { AccountsService } from "./accounts/accounts.service";
declare module "slim-rpc/lib/cjs/models" {
interface RpcContext {
user_id:string;
org_id:string;
services: {
users: UserService;
accounts:AccountsService
};
}
}
Usage example
In this example we use express as a web framework and we have 3 modules
- auth
- users
- accounts
// server/index.ts
import express from 'express'
import { create_rpc_server, RpcExpressAdapter } from "slim-rpc";
import { auth } from './auth/auth.rpc'
import { accounts } from './accounts/accounts.rpc'
import { create_accounts_service } from './accounts/accounts.service'
import { users } from './users/users.rpc'
import { create_user_service } from './users/users.service'
const app = express();
create_rpc_server({
web_framework: RpcExpressAdapter(app),
routes: {
auth,
accounts,
users,
},
create_context: async (req) => {
return {
org_id:req.headers['org-id'],
users_id:req.headers['user-id'],
services: {
users: create_user_service(req),
users: create_accounts_service(req),
},
};
},
});
// server/users/users.rpc.ts
import { RPC } from "slim-rpc";
import {z} from 'zod'
const user_scheme = z.object({
name: z.string(),
age: z.number(),
});
const list = RPC<{ count: number }, User[]>(
z.object({ count: z.number().min(6) }),
async ({ count }, { ctx }) => {
const users_col = await ctx.services.users();
const users = await users_col.list();
return users.slice(0, count);
}
);
const update = RPC<User, User[]>(
user_scheme,
async ({ age, name }, { ctx }) => {
const users_col = await ctx.services.users();
await users_col.create(name, age);
const users_state = await users_col.list();
return users_state;
}
);
const create = RPC<{ name: string; age: number }, { id: string }>(
user_scheme,
async ({ age, name }, { ctx }) => {
const users_col = await ctx.services.users();
const id = await users_col.create(name, age);
return { id };
}
);
const remove = RPC<{ id: string }, User[]>(
z.object({ id: z.string() }),
async ({ id }, { ctx }) => {
const users_col = await ctx.services.users();
await users_col.remove(id);
const users = await users_col.list();
return users;
}
);
export const users = {
list,
update,
create,
remove,
};
Error handling
input validation errors (400) are handled by the library using the error msgs from the zod scheme parser other errors will be returned as status 500 with the massage taken from the error object. 401 & 403 for the time being I suggest that those errors will be handled by middlewares before Slim-RPC is handling them. for ex when using express:
// handle 401
app.use(async (req,req,next)=>{
if(req.path === '/login'){
next();
}else{
const ia_authorized = await check_is_auth_user(req);
if(!is_authorized){
res.status(401).send('user is not authorized')
}else{
next();
}
}
})
// handle 403
// I want the library to have the RPC function handle the role based calls
// will be on the road map & impl soon I hope
app.use(async (req,req,next)=>{
if(req.path == '/login'){
next();
}else{
const role = extract_role(req);
const is_allowed = check_is_user_allowed(role,req.path);
if(is_allowed){
next()
}else{
res.status(403).send("user doesn't have enough permission to perform the action")
}
}
})
// then init SlimRPC
Shared Router model
// router.model.ts
import { accounts } from "./server/accounts/accounts.rpc";
import { users } from "./server/users/users.rpc";
import {auth} from './server/auth/auth.rpc'
const appRouter = {
auth,
accounts,
users,
};
export type AppRouter = typeof appRouter;
Client
Usage example
// client/state.ts
import { AppRouter } from "../router.model";
import { create_client,set_rpc_client_config } from "slim-rpc";
const client = create_client<AppRouter>(env.base_url);
const all_clients = ref([]); // vuejs example
const init = async(name:string,pass:string)=>{
// get the authorization Bearer by login
const auth_res = await client.auth.login({name,pass})
if(auth_res.type == 'success'){
set_rpc_client_config({
headers: {
authorization: auth_res.value,
},
});
// get a list of 20 users
const res = await client.users.list.query({count:20}) // full intellisense !
if(res.type == 'success'){
all_clients.value = res.value;
}
}
}
// ...
Error handling
the RpcResponse response object for each call is of the form:
interface RpcSuccessResponse<T> {
type: "success";
value: T;
}
interface RpcErrorResponse {
type: "error";
code: number;
reason?: string;
}
export type RpcResponse<T> = RpcSuccessResponse<T> | RpcErrorResponse;
so each error will be of type == 'error' and will have at least a code value.
Roadmap
- Handle 401 globally
- Enable handling of 403 (role based) within each RPC call definition
- koa and fastify adapters
- Batching mechanism for all in one go client-server round trip
- Canceling requests mechanism
- Server Sent events mechanism
Contributions
Not yet