enders-sync
v0.2.5
Published
A Fullstack RPC library connecting your backend to your frontend seemlessly over a REST API
Maintainers
Readme
Enders-Sync
Enders (Backenders + Frontenders) Sync
A zero-boilerplate RPC (Remote Procedure Call) Fullstack library for Express.js that makes calling server functions from the client feel like calling local functions.
Features
- 🏆 Fullstack: both server-side and client-side libraries
- 🚀 Zero Boilerplate: Call server functions directly without writing fetch code
- 🔒 Built-in Authentication: Cookie-based auth with customizable validators
- 🎯 Type-Safe: Full TypeScript support
- 🪶 Lightweight: Minimal dependencies
- 🔄 Promise-Based: Works seamlessly with async/await
Table of Content
- Enders-Sync
Installation
on the server:
npm install enders-syncon the client:
npm install enders-sync-clientQuick Start
Server Setup
import express from 'express';
import { createRPC } from 'enders-sync';
const app = express();
app.use(express.json());
// Create a public RPC endpoint (no authentication required)
const publicRPC = createRPC(app, '/api/public', (req) => ({
success: true,
metadata: { auth: { role: 'public' } }
}));
// Register your functions
publicRPC.add(function getUser(metadata, userId) {
return { id: userId, name: 'John Doe', email: '[email protected]' };
});
publicRPC.add(function calculateSum(metadata, a, b) {
return a + b;
});
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});Client Setup
api.js:
import { RPC } from 'enders-sync-client';
// Create RPC client instance
export const api = new RPC('/api/public');
// Load available functions (call once on app initialization)
api.load('getUser', 'calculateSum');
// Now call server functions as if they were local!
const user = await api.getUser(123);
console.log(user); // { id: 123, name: 'John Doe', email: '[email protected]' }
const sum = await api.calculateSum(5, 10);
console.log(sum); // 15App.jsx:
React Example
import { useEffect, useState } from 'react';
import { api } from './api';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
api.getUser(userId)
.then(setUser)
.catch(console.error)
.finally(() => setLoading(false));
}, [userId]);
if (loading) return <div>Loading...</div>;
return <div>{user.name}</div>;
}Next JS example
// app/users/[id]/page.js
import { api } from './api';
export default async function UserProfile({ params }) {
const user = await api.getUser(params.id);
return (
<div>
<h1>{user.name}</h1>
{/* More user data */}
</div>
);
}Authentication
The validator function receives the full Express Request object, allowing you to access cookies, headers, query parameters, and more for authentication and validation.
import express from 'express';
import jwt from 'jsonwebtoken';
import { createRPC } from 'enders-sync';
const app = express();
app.use(express.json());
// Create a validator for your Auth and access control
function authUser(req) {
try {
// Access cookies (automatically parsed by enders-sync)
const token = req.cookies.auth_token;
if (!token) {
return { success: false };
}
const decoded = jwt.verify(token, process.env.JWT_SECRET);
return {
success: true,
metadata: {
auth: {
userId: decoded.userId,
role: decoded.role
}
}
};
} catch (error) {
return { success: false };
}
}
const authenticatedRPC = createRPC(
app,
'/api/user',
authUser
);
// Access auth metadata in your functions
authenticatedRPC.add(function getMyProfile(metadata) {
const userId = metadata.auth.userId;
// Fetch user profile using authenticated userId
return { id: userId, name: 'Current User' };
});Accessing Request and Response
The metadata object passed to your RPC handlers includes the Express req and res objects, giving you full control when needed:
publicRPC.add(function specialFunction(metadata, data) {
// Access the Express request object
const userAgent = metadata.req.headers['user-agent'];
const clientIp = metadata.req.ip;
// Access auth data
const userId = metadata.auth.userId;
// You can even manipulate the response
metadata.res.setHeader('X-Custom-Header', 'value');
return { processed: data };
});Using Express Middleware
let's taking adding a rate limiter as an example
import rateLimit from 'express-rate-limit'; // import rate limiting library here here
import express from 'express';
import { createRPC } from 'enders-sync';
// setting up rate limiter
const limiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 10, // limit each IP to 10 requests per windowMs
message: { error: 'Too many requests, slow down!' }
});
const app = express();
app.use(express.json());
// using middleware for the whole RPC path
app.use('/api/public', limiter);
// Create a public RPC endpoint (no authentication required)
const publicRPC = createRPC(app, '/api/public', (req) => ({
success: true,
metadata: { auth: { role: 'public' } }
}));
Security Considerations
- Always validate and sanitize RPC function inputs
- Use different validators for different permission levels
- Consider rate limiting for public endpoints
- The auth metadata is trusted - ensure your validator is secure
- Validator receives full Express Request object - be cautious about what you expose
- Cookie parsing is handled automatically by the library
Multiple RPC Endpoints
// Public API (no auth)
const publicRPC = createRPC(app, '/api/public', (req) => ({
success: true,
metadata: { auth: {} }
}));
// User API (requires authentication)
const userRPC = createRPC(app, '/api/user', validateUserToken);
// Admin API (requires admin role)
const adminRPC = createRPC(app, '/api/admin', validateAdminToken);Client:
import { RPC } from "enders-sync-client"
export const publicAPI = new RPC('/api/public');
export const userAPI = new RPC('/api/user');
export const adminAPI = new RPC('/api/admin');
// define all RPC methods
publicAPI.load('getUser', 'calculateSum');
userAPI.load('getUserProfile');
adminAPI.load('getAdminProfile');
API Reference
Server API
createRPC(app, path, validator)
Creates an RPC endpoint on your Express app.
Parameters:
app(Express): Your Express application instancepath(string): Base path for the RPC endpoint (e.g.,/api/public)validator(Validator): Authentication validator function
Returns: RPC instance
Validator Function:
type Validator = (req: Request) => {
success: boolean;
metadata?: {
auth: Record<string, string | number>;
};
}The validator receives the full Express Request object with cookies already parsed. Return success: true with optional metadata to allow the request, or success: false to reject it.
RPC.add(functionHandler, optionalName?)
Registers a function to be callable via RPC.
Parameters:
functionHandler(Function): The function to registeroptionalName(string, optional): Custom name for the function (defaults to function.name)
Requirements:
- Function must be a named function (not arrow function) unless you provide
optionalName - First parameter must be
metadata(containsauth,req, andres) - Remaining parameters are the RPC call arguments
// Using function name
rpc.add(function myFunction(metadata, param1, param2) {
// Your logic here
return result;
});
// Using custom name
const handler = function(metadata, param1) {
return result;
};
rpc.add(handler, 'customName');RPC.dump()
Returns an array of all registered function names.
const functions = rpc.dump();
console.log(functions); // ['getUser', 'calculateSum', ...]Client API
new RPC(url)
Creates a new RPC client instance.
Parameters:
url(string): Base URL of the RPC endpoint (e.g.,/api/public)
rpc.load( ...methods )
declares the specified RPC functions from the server. Must be called before using any remote functions.
Parameters:
methods(string[]): List of function names to load
rpc.call(name, params)
Manually call an RPC function (usually not needed - use auto-generated methods instead).
Parameters:
name(string): Function nameparams(Array): Function parameters
Returns: Promise<any>
Endpoints
When you create an RPC endpoint at /api/public, one route is automatically created:
POST /api/public/call- Executes RPC calls
Error Handling
Server-Side Errors
publicRPC.add(function riskyOperation(metadata, data) {
if (!data) {
throw new Error('Data is required');
}
// Process data
return result;
});Client-Side Error Handling
try {
const result = await api.riskyOperation(null);
} catch (error) {
console.error('RPC Error:', error.message);
// Handle error appropriately
}
// Or with promises
api.riskyOperation(data)
.then(result => console.log('Success:', result))
.catch(error => console.error('Error:', error));TypeScript Support
Server-Side Types
import { type Metadata, type RPCHandler } from 'enders-sync';
import { type Request } from 'express';
interface User {
id: number;
name: string;
email: string;
}
const getUser: RPCHandler = function(
metadata: Metadata,
userId: number
): User {
return {
id: userId,
name: 'John Doe',
email: '[email protected]'
};
};
// Custom validator with types
const myValidator = (req: Request): ValidatorReturn => {
const token = req.cookies.token;
if (!token) {
return { success: false };
}
return {
success: true,
metadata: {
auth: {
userId: 123,
role: 'admin'
}
}
};
};
// Register the function
publicRPC.add(getUser);Client-Side Types
import { RPC } from 'enders-sync-client';
interface User {
id: number;
name: string;
email: string;
}
export interface PublicAPI {
getUser(userId: number): Promise<User>;
calculateSum(a: number, b: number): Promise<number>;
}
export const public_api = new RPC('/api/public') as unknown as PublicAPI;
public_api.load('getUser', 'calculateSum');
// Now you get full type safety!
const user: User = await public_api.getUser(123);Best Practices
- Initialize once: Call
api.load( ...methods )to declare available RPC methods globally - Error handling: Always handle errors from RPC calls
- Named functions: Use named functions (not arrow functions) for RPC handlers, or provide custom names
- Validation: Validate input parameters in your RPC functions
- Authentication: Use different RPC endpoints for different permission levels
- Async operations: RPC handlers can be async functions
- Metadata structure: Always structure your validator metadata with an
authproperty - Cookie parsing: Cookies are automatically parsed - access them via
req.cookies
Example: Complete App
server.js:
import express from 'express';
import { createRPC } from 'enders-sync';
const app = express();
app.use(express.json());
const publicRPC = createRPC(app, '/api/public', (req) => ({
success: true,
metadata: { auth: {} }
}));
// Database mock
const users = [
{ id: 1, name: 'Alice', email: '[email protected]' },
{ id: 2, name: 'Bob', email: '[email protected]' }
];
publicRPC.add(function getUsers(metadata) {
return users;
});
publicRPC.add(function getUserById(metadata, id) {
const user = users.find(u => u.id === id);
if (!user) throw new Error('User not found');
return user;
});
publicRPC.add(async function searchUsers(metadata, query) {
// Simulate async database query
await new Promise(resolve => setTimeout(resolve, 100));
return users.filter(u =>
u.name.toLowerCase().includes(query.toLowerCase())
);
});
// Example with request/response access
publicRPC.add(function getClientInfo(metadata) {
return {
ip: metadata.req.ip,
userAgent: metadata.req.headers['user-agent']
};
});
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});api.js:
import { RPC } from 'enders-sync-client';
export const api = new RPC('/api/public');app.js:
import { api } from './api.js';
// Initialize
api.load('getUsers', 'getUserById', 'searchUsers', 'getClientInfo');
// Use anywhere in your app
const users = await api.getUsers();
console.log(users);
const alice = await api.getUserById(1);
console.log(alice);
const results = await api.searchUsers('bob');
console.log(results);
const clientInfo = await api.getClientInfo();
console.log(clientInfo);License
MIT © Hussein Layth Al-Madhachi
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
