@pineliner/uni-rest
v1.0.7
Published
Unified type-safe REST API framework for seamless client/server communication
Readme
uni-rest
Type-safe REST API framework with shared contracts between client and server.
Design Philosophy
- Single endpoint - One POST endpoint handles all requests
- Instruction-based routing -
{ from: "resource", instruction: { name: "action", payload: {...} } } - Shared contracts - Single source of truth using Zod schemas
- Type inference - Full TypeScript types from contracts
- Zero boilerplate - Automatic validation and serialization
Quick Start
1. Define Contract
// contracts/products.contract.ts
import { z } from 'zod'
import { defineContract } from '@pineliner/uni-rest'
const ProductSchema = z.object({
id: z.number(),
name: z.string(),
price: z.number()
})
export const productsContract = defineContract('products', {
getAll: {
input: z.object({ limit: z.number().optional() }),
output: z.array(ProductSchema)
},
getById: {
input: z.object({ id: z.number() }),
output: ProductSchema
},
create: {
input: ProductSchema.omit({ id: true }),
output: ProductSchema
}
})2. Implement Server
// server/products.handlers.ts
import { createHandlers } from '@pineliner/uni-rest/server'
import { productsContract } from '../contracts/products.contract'
export const productsHandlers = createHandlers(productsContract, {
getAll: async ({ limit = 50 }, req, ctx) => {
const products = await db.query('SELECT * FROM products LIMIT ?', [limit])
return products
},
getById: async ({ id }, req, ctx) => {
const product = await db.query('SELECT * FROM products WHERE id = ?', [id])
if (!product) throw new Error('Not found')
return product
},
create: async (data, req, ctx) => {
const result = await db.insert('products', data)
return { id: result.insertId, ...data }
}
})// server/index.ts
import { createManager } from '@pineliner/uni-rest/server'
import { productsHandlers } from './products.handlers'
import express from 'express'
const app = express()
const manager = createManager()
manager.register('products', productsHandlers)
app.use('/api/v1', manager.middleware())
app.listen(3000)3. Use Client
// client/products.service.ts
import { createClient } from '@pineliner/uni-rest/client'
import { productsContract } from '../contracts/products.contract'
const client = createClient(productsContract, {
baseUrl: '/api/v1'
})
// Fully typed!
const products = await client.getAll({ limit: 10 })
const product = await client.getById({ id: 1 })
const newProduct = await client.create({ name: 'Test', price: 99.99 })Request/Response Format
Request
{
"from": "products",
"instruction": {
"name": "getAll",
"payload": { "limit": 10 }
}
}Success Response
{
"status": "success",
"data": [...]
}Error Response
{
"status": "error",
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid input",
"details": {...}
}
}API Reference
defineContract(name, endpoints)
Define a contract with Zod schemas:
const contract = defineContract('users', {
list: {
input: z.object({ page: z.number() }),
output: z.array(UserSchema)
}
})createHandlers(contract, handlers)
Implement typed handlers:
const handlers = createHandlers(contract, {
list: async ({ page }, req, ctx) => {
return await fetchUsers(page)
}
})Handler signature: (input, req, ctx) => Promise<output>
createManager()
Create manager for single endpoint:
const manager = createManager()
manager.register('products', productsHandlers)
manager.register('users', usersHandlers)
app.use('/api', manager.middleware())createClient(contract, config)
Create typed client:
const client = createClient(contract, {
baseUrl: '/api/v1'
})
await client.actionName(input)Features
- ✅ Type-safe contracts with Zod
- ✅ Automatic input validation
- ✅ Single endpoint architecture
- ✅ Full TypeScript inference
- ✅ Express middleware integration
- ✅ Error handling with status codes
- ✅ Context injection (auth, db, etc.)
