TypeScript Quickstart
Build a complete PulseRPC service in TypeScript with our e-commerce checkout example.
Prerequisites
- Node.js 18 or later
- TypeScript 5.0 or later
- PulseRPC CLI installed (Installation Guide)
1. Define the Service (2 min)
Create checkout.pulse with your service definition:
namespace checkout
// Enums for order status and payment methods
enum OrderStatus {
pending
paid
shipped
delivered
cancelled
}
enum PaymentMethod {
credit_card
debit_card
paypal
apple_pay
}
// Core domain entities
struct Product {
productId string
name string
description string
price float
stock int
imageUrl string [optional]
}
struct CartItem {
productId string
quantity int
price float
}
struct Cart {
cartId string
items []CartItem
subtotal float
}
struct Address {
street string
city string
state string
zipCode string
country string
}
struct Order {
orderId string
cart Cart
shippingAddress Address
paymentMethod PaymentMethod
status OrderStatus
total float
createdAt int
}
// Request/Response structures
struct AddToCartRequest {
cartId string [optional]
productId string
quantity int
}
struct CreateOrderRequest {
cartId string
shippingAddress Address
paymentMethod PaymentMethod
}
struct CheckoutResponse {
orderId string
message string [optional]
}
// Error declarations for createOrder
errors {
1001 CartNotFound "Cart doesn't exist"
1002 CartEmpty "Cart has no items"
1003 PaymentFailed "Payment method rejected"
1004 OutOfStock "Insufficient inventory"
1005 InvalidAddress "Shipping address validation failed"
}
// Service interfaces
interface CatalogService {
// Returns a list of all available products
listProducts() []Product
// Returns details for a specific product, or null if not found
getProduct(productId string) Product [optional]
}
interface CartService {
// Adds an item to the cart (creates cart if cartId not provided)
addToCart(request AddToCartRequest) Cart
// Returns the cart contents, or null if cart doesn't exist
getCart(cartId string) Cart [optional]
// Removes all items from the cart, returns true if successful
clearCart(cartId string) bool
}
interface OrderService {
// Converts a cart to an order
createOrder(request CreateOrderRequest) CheckoutResponse raises(CartNotFound, CartEmpty, PaymentFailed, OutOfStock, InvalidAddress)
// Returns the order details, or null if order doesn't exist
getOrder(orderId string) Order [optional]
}Defining Service Errors
The IDL uses the errors keyword to declare error codes and raises() clauses to specify which errors each method can raise:
errors {
1001 CartNotFound "Cart doesn't exist"
1002 CartEmpty "Cart has no items"
1003 PaymentFailed "Payment method rejected"
1004 OutOfStock "Insufficient inventory"
1005 InvalidAddress "Shipping address validation failed"
}
interface OrderService {
createOrder(request CreateOrderRequest) CheckoutResponse raises(CartNotFound, CartEmpty, PaymentFailed, OutOfStock, InvalidAddress)
}
For details on error handling, see Error Handling.
2. Generate Code (1 min)
Generate the TypeScript code from your IDL:
pulserpc -plugin ts-client-server checkout.pulse
This creates:
checkout.ts- Type definitionsserver.ts- PulseRPC server frameworkclient.ts- HTTP client frameworkpulserpc/- Runtime library
The IDL is embedded directly in server.ts for the pulserpc-idl RPC method.
3. Implement the Server (10-15 min)
Create my_server.ts that implements your service handlers:
import { readFileSync } from 'fs';
import { Server, Contract } from './pulserpc';
import { CatalogService, CartService, OrderService } from './server';
import { RPCError } from './pulserpc/rpc';
import * as http from 'http';
const products = [
{
productId: 'prod001',
name: 'Wireless Mouse',
description: 'Ergonomic mouse',
price: 29.99,
stock: 50,
imageUrl: 'https://example.com/mouse.jpg'
},
{
productId: 'prod002',
name: 'Mechanical Keyboard',
description: 'RGB keyboard',
price: 89.99,
stock: 25,
imageUrl: 'https://example.com/keyboard.jpg'
}
];
const carts = new Map<string, any>();
const orders = new Map<string, any>();
class CatalogServiceImpl extends CatalogService {
listProducts(_ctx: Record<string, any>): any[] {
return products;
}
getProduct(_ctx: Record<string, any>, productId: string): any | null {
return products.find((p: any) => p.productId === productId) || null;
}
}
class CartServiceImpl extends CartService {
addToCart(_ctx: Record<string, any>, request: any): any {
let cartId = request.cartId || `cart_${Math.floor(Math.random() * 9000 + 1000)}`;
let cart = carts.get(cartId);
if (!cart) {
cart = {
cartId,
items: [],
subtotal: 0
};
carts.set(cartId, cart);
}
const product = products.find((p: any) => p.productId === request.productId);
if (!product) {
throw new RPCError(-32602, 'Product not found');
}
cart.items.push({
productId: request.productId,
quantity: request.quantity,
price: product.price
});
cart.subtotal = cart.items.reduce((sum: number, item: any) => sum + item.price * item.quantity, 0);
return cart;
}
getCart(_ctx: Record<string, any>, cartId: string): any | null {
return carts.get(cartId) || null;
}
clearCart(_ctx: Record<string, any>, cartId: string): boolean {
const cart = carts.get(cartId);
if (cart) {
cart.items = [];
cart.subtotal = 0;
return true;
}
return false;
}
}
class OrderServiceImpl extends OrderService {
createOrder(_ctx: Record<string, any>, request: any): any {
const cart = carts.get(request.cartId);
if (!cart) {
throw new RPCError(1001, 'CartNotFound: Cart does not exist');
}
if (!cart.items || cart.items.length === 0) {
throw new RPCError(1002, 'CartEmpty: Cannot create order from empty cart');
}
const orderId = `order_${Math.floor(Math.random() * 90000 + 10000)}`;
const order = {
orderId,
cart,
shippingAddress: request.shippingAddress,
paymentMethod: request.paymentMethod,
status: 'pending',
total: cart.subtotal,
createdAt: Math.floor(Date.now() / 1000)
};
orders.set(orderId, order);
return { orderId, message: 'Order created successfully' };
}
getOrder(_ctx: Record<string, any>, orderId: string): any | null {
return orders.get(orderId) || null;
}
}
const idlData = JSON.parse(readFileSync('idl.json', 'utf-8'));
const contract = new Contract(idlData);
const rpcServer = new Server({ contract, validateRequests: true, validateResponses: true });
rpcServer.addHandler('CatalogService', new CatalogServiceImpl());
rpcServer.addHandler('CartService', new CartServiceImpl());
rpcServer.addHandler('OrderService', new OrderServiceImpl());
class RPCHandler {
private rpcServer: Server;
constructor(rpcServer: Server) {
this.rpcServer = rpcServer;
}
handle(req: http.IncomingMessage, res: http.ServerResponse): void {
let body = '';
req.on('data', (chunk) => { body += chunk.toString(); });
req.on('end', () => {
try {
const data = JSON.parse(body);
const response = this.rpcServer.call(data);
if (response === null || response === undefined) {
res.writeHead(204);
res.end();
} else {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(response));
}
} catch (err: any) {
const errorResponse = {
jsonrpc: '2.0',
error: { code: -32700, message: `Parse error: ${err.message}` },
id: null,
};
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(errorResponse));
}
});
}
}
const port = parseInt(process.env.SERVER_PORT || '8080', 10);
const handler = new RPCHandler(rpcServer);
const httpServer = http.createServer((req, res) => {
if (req.method === 'POST') {
handler.handle(req, res);
} else {
res.writeHead(405, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Method Not Allowed' }));
}
});
httpServer.listen(port, '0.0.0.0', () => {
console.log(`Server listening on http://0.0.0.0:${port}`);
});Create a package.json file in the same directory:
{
"name": "checkout-service",
"version": "1.0.0",
"type": "commonjs",
"scripts": {
"build": "tsc",
"start": "node dist/my_server.js"
},
"dependencies": {
"pulserpc-ts-runtime": "file:./pulserpc"
},
"devDependencies": {
"@types/node": "^18.0.0",
"typescript": "^5.0.0"
}
}
Create a tsconfig.json file in the same directory:
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"lib": ["ES2020"],
"types": ["node"],
"moduleResolution": "node",
"esModuleInterop": true,
"skipLibCheck": true,
"strict": false,
"resolveJsonModule": true,
"outDir": "./dist",
"rootDir": "."
},
"include": ["*.ts"],
"exclude": ["node_modules", "dist"]
}
Build and start your server:
npm install
npm run build
npm start
4. Implement the Client (5-10 min)
Create my_client.ts to call your service:
import { HTTPTransport, CatalogServiceClient, CartServiceClient, OrderServiceClient } from './client';
const transport = new HTTPTransport('http://localhost:8080');
const catalog = new CatalogServiceClient(transport);
const cart = new CartServiceClient(transport);
const orders = new OrderServiceClient(transport);
async function main() {
const products = await catalog.listProducts();
console.log('=== Products ===');
for (const p of products) {
console.log(`${p.name} - $${p.price}`);
}
const result = await cart.addToCart({
cartId: null,
productId: products[0].productId,
quantity: 2
});
console.log(`\nCart: ${result.cartId}`);
const response = await orders.createOrder({
cartId: result.cartId,
shippingAddress: {
street: '123 Main St',
city: 'San Francisco',
state: 'CA',
zipCode: '94105',
country: 'USA'
},
paymentMethod: 'credit_card'
});
console.log(`✓ Order created: ${response.orderId}`);
}
main().catch(console.error);Build and run your client:
npm run build
node dist/my_client.js
Error Codes
Throw RPCError with custom error codes:
throw new RPCError(1002, 'CartEmpty: Cannot create order from empty cart');
| Code | Name |
|---|---|
| 1001 | CartNotFound |
| 1002 | CartEmpty |
| 1003 | PaymentFailed |
| 1004 | OutOfStock |
| 1005 | InvalidAddress |
Next Steps
- TypeScript Reference - Type mappings and async patterns
- IDL Syntax - Full IDL reference