@usirin/spellbook
Version:
Type-safe API surfaces that work across process boundaries
280 lines (227 loc) • 7.7 kB
Markdown
# @usirin/spellbook
Type-safe API surfaces that work across process boundaries.
[](https://www.npmjs.com/package/@usirin/spellbook)
[](LICENSE)
## Overview
Spellbook lets you create type-safe APIs with full validation and error handling. It provides a consistent way to define your API surface once and use it anywhere.
- **Type-Safe**: End-to-end TypeScript type safety from definition to usage
- **Validated**: Schema validation with any [Standard Schema](https://standardschema.dev/) compatible library
- **Composable**: Build complex APIs from simple, reusable operations
- **Consistent**: Standard patterns for defining and consuming APIs
- **Context-Aware**: Share context across API operations
## Installation
```bash
npm install @usirin/spellbook # npm
yarn add @usirin/spellbook # yarn
pnpm add @usirin/spellbook # pnpm
bun add @usirin/spellbook # bun
```
## Quick Start: TodoMVC API
Here's how to create a type-safe API for a TodoMVC application:
```typescript
import { z } from "zod";
import { createSpell, createSpellbook } from "@usirin/spellbook";
// 1. Define your Todo schema
const TodoSchema = z.object({
id: z.string().uuid(),
text: z.string().min(1).max(200),
completed: z.boolean().default(false),
createdAt: z.string().datetime(),
});
// 2. Define the context shared across all spells
const contextSchema = z.object({
db: z.any(), // Your database connection
logger: z.any(), // Logging utilities
});
// 3. Create your spells
const listTodos = createSpell({
description: "Get all todo items",
parameters: z.object({}), // No parameters needed to list all
result: z.object({
todos: z.array(TodoSchema),
}),
context: contextSchema,
execute: async ({}, { db, logger }) => {
logger.info(`Listing all todos`);
// Implementation would query from database
const todos = [
{ id: "abc-123", text: "Learn Spellbook", completed: true, createdAt: "2023-01-01T12:00:00Z" },
{ id: "def-456", text: "Build TodoMVC API", completed: false, createdAt: "2023-01-02T12:00:00Z" },
];
// Return all todos directly
return { todos };
}
});
const createTodo = createSpell({
description: "Add a new todo item",
parameters: z.object({
text: z.string().min(1).max(200),
}),
result: TodoSchema,
context: contextSchema,
execute: async ({ text }, { db, logger }) => {
logger.info(`Creating new todo: ${text}`);
// Implementation would save to database
return {
id: crypto.randomUUID(),
text,
completed: false,
createdAt: new Date().toISOString(),
};
}
});
// Spells for toggleTodo, deleteTodo, clearCompleted removed for brevity
// 4. Create the shared context instance
const context = {
db: { /* your database connection */ },
logger: {
info: (message) => console.log(`[INFO] ${message}`),
error: (message) => console.error(`[ERROR] ${message}`)
}
};
// 5. Create your API with context
const todoAPI = createSpellbook({
listTodos, // Use camelCase keys
createTodo,
}, context);
// 6. Export the API type for consumers
export type TodoAPI = typeof todoAPI;
async function todoOperations() {
// Add a new todo with validation
const newTodo = await todoAPI.createTodo({ // Use camelCase access
text: "Build an API with Spellbook",
});
// TypeScript knows the exact shape of newTodo
console.log(`Created: ${newTodo.text} (${newTodo.id})`);
// Get all todos
const { todos } = await todoAPI.listTodos({}); // Use camelCase access, no filter
console.log(`Fetched ${todos.length} todos.`);
// Examples for toggle, delete, validation removed for brevity
}
```
Your API can be used in any JS/TS environment - frontend frameworks like React, Vue or Angular, or backend environments like Node.js, Deno, or Bun.
## Core Concepts
### Spells
A spell is a single API operation with parameters, result schema, context, and implementation:
```typescript
const getUserSpell = createSpell({
description: "Get user by ID",
parameters: z.object({
id: z.string().uuid(),
}),
result: z.object({
id: z.string().uuid(),
username: z.string(),
email: z.string().email(),
profile: z.object({
fullName: z.string(),
avatarUrl: z.string().url().optional(),
}),
}),
context: z.object({
db: z.any(),
authManager: z.any()
}),
execute: async ({ id }, { db, authManager }) => {
// Implementation would fetch user from database
return {
id,
username: "johndoe",
email: "john@example.com",
profile: {
fullName: "John Doe",
avatarUrl: "https://example.com/avatar.jpg",
},
};
}
});
```
Key aspects of a spell:
- **Description**: Human-readable description of what the operation does
- **Parameters**: Input schema with validation rules
- **Result**: Output schema defining the expected return type
- **Context**: Shared resources and dependencies required by the spell
- **Execute**: Implementation function that performs the operation
### Context Handling
Spellbook provides a robust system for managing shared context across your API:
```typescript
// Define context schema shared by multiple spells
const apiContext = z.object({
db: z.any(),
logger: z.any(),
config: z.object({
apiVersion: z.string(),
maxItems: z.number()
})
});
// Create spells with the shared context
const listItems = createSpell({
// ...other properties
context: apiContext,
execute: async (params, { db, logger, config }) => {
logger.info(`Listing items with max: ${config.maxItems}`);
// Implementation
}
});
// Create a context instance with actual dependencies
const contextInstance = {
db: new Database(),
logger: new Logger(),
config: {
apiVersion: "v1",
maxItems: 100
}
};
// Create a spellbook with the context
const api = createSpellbook({
listItems,
// ...other spells
}, contextInstance);
```
The context is:
- **Type-safe**: TypeScript infers and validates the context structure
- **Automatic**: Provided to each spell without manually passing it
- **Validated**: Context validation happens at runtime
- **Shared**: Reuse the same dependencies across operations
### Spellbooks
A spellbook is a collection of related spells forming a complete API surface with shared context:
```typescript
const userAPI = createSpellbook({
getUser: getUserSpell,
createUser: createUserSpell,
updateUser: updateUserSpell,
deleteUser: deleteUserSpell,
}, contextInstance);
```
Spellbooks allow you to:
- Group related operations
- Share context across operations
- Export a single API type for consumers
- Provide a unified interface for execution
### Schema Validation
Spellbook leverages [Standard Schema](https://standardschema.dev/) for validation, supporting multiple validation libraries:
```typescript
// With Zod
import { z } from "zod";
const zodSpell = createSpell({
parameters: z.object({ email: z.string().email() }),
// ...
});
// With Valibot
import * as v from "valibot";
const valibotSpell = createSpell({
parameters: v.object({ email: v.string([v.email()]) }),
// ...
});
```
Benefits of Standard Schema integration:
- Use your preferred validation library
- Full type inference for parameters and results
- Runtime validation for both inputs and outputs, handled automatically within the spell's `execute` method
- Consistent error handling
This type system enables:
- Complete inference of parameter and result types
- Automatic derivation of context types
- Type-safe API consumption
## License
[MIT](LICENSE)