# TypeScript Type-Safe API Guide
This guide covers the improved type-safe TypeScript API for the PostgREST parser.
## Overview
The PostgREST parser now provides two APIs:
1. **Type-Safe Client API** (Recommended) - `client.ts`
- Fully typed with zero `any` types
- Object-based APIs (no JSON string manipulation)
- Better IntelliSense and autocomplete
- Idiomatic TypeScript patterns
2. **Low-Level WASM API** - `postgrest_parser.js`
- Auto-generated by wasm-bindgen
- Direct WASM bindings
- Use only when you need low-level control
## Installation
```bash
npm install postgrest-parser
# or
yarn add postgrest-parser
# or
pnpm add postgrest-parser
```
## Quick Start
### Using the Type-Safe Client (Recommended)
```typescript
import { createClient } from 'postgrest-parser';
import type { QueryResult } from 'postgrest-parser/types';
const client = createClient();
// SELECT query
const result: QueryResult = client.select("users", {
filters: { "age": "gte.18", "status": "eq.active" },
order: ["created_at.desc"],
limit: 10
});
console.log(result.query); // SQL query string
console.log(result.params); // ["18", "active"]
console.log(result.tables); // ["users"]
```
### Using the Low-Level WASM API
```typescript
import { parseQueryString } from 'postgrest-parser/wasm';
const result = parseQueryString("users", "age=gte.18&status=eq.active");
console.log(result.query);
console.log(result.params);
```
## Type Safety Improvements
### Before: Auto-Generated WASM Bindings
```typescript
// ❌ Too many 'any' types - no type safety
export class WasmQueryResult {
toJSON(): any; // Unknown return type
readonly params: any; // No idea what params can be
readonly tables: any; // Could be anything
}
// ❌ Redundant optionals
parseDelete(table: string, query_string: string, headers?: string | null)
// ^^^^^^^^^^^^
// ❌ Loose string type for HTTP methods
parseRequest(method: string, path: string, ...)
// ^^^^^^ - Any string accepted, no validation
```
### After: Type-Safe Client API
```typescript
// ✅ Fully typed with proper interfaces
export interface QueryResult {
query: string;
}
// ✅ Clean optional parameters
parseDelete(table: string, queryString: string, headers?: RequestHeaders)
// ^^^^^^^^^ - Properly optional
// ✅ Strict HTTP method type
parseRequest(method: HttpMethod, path: string, ...)
## API Comparison
### SELECT Queries
#### WASM API (Old)
```typescript
import { parseQueryString } from 'postgrest-parser/wasm';
const result = parseQueryString(
"users",
"age=gte.18&status=eq.active&order=created_at.desc&limit=10"
);
// ❌ Manual query string construction
// ❌ No type checking on filters
// ❌ Easy to make syntax errors
```
#### Client API (New)
```typescript
import { createClient } from 'postgrest-parser';
const client = createClient();
const result = client.select("users", {
filters: {
age: "gte.18",
status: "eq.active"
},
order: ["created_at.desc"],
limit: 10
});
// ✅ Object-based configuration
// ✅ IntelliSense for all options
// ✅ No manual string manipulation
```
### INSERT Queries
#### WASM API (Old)
```typescript
import { parseInsert } from 'postgrest-parser/wasm';
const result = parseInsert(
"users",
JSON.stringify({ name: "Alice", email: "alice@example.com" }),
"returning=id,name",
JSON.stringify({ Prefer: "return=representation" })
);
// ❌ Manual JSON stringification
// ❌ Headers passed as JSON string
// ❌ Query string concatenation
```
#### Client API (New)
```typescript
import { createClient } from 'postgrest-parser';
const client = createClient();
const result = client.insert("users", {
name: "Alice",
email: "alice@example.com"
}, {
returning: ["id", "name"], // or "id,name"
prefer: { return: "representation" }
});
// ✅ Native objects, no stringification
// ✅ Typed prefer options
// ✅ Array or string for returning
```
### UPDATE Queries
#### WASM API (Old)
```typescript
import { parseUpdate } from 'postgrest-parser/wasm';
const result = parseUpdate(
"users",
JSON.stringify({ status: "active" }),
"id=eq.123",
null
);
// ❌ Filters as query string
// ❌ Body as JSON string
```
#### Client API (New)
```typescript
import { createClient } from 'postgrest-parser';
const client = createClient();
const result = client.update("users", {
status: "active"
}, {
id: "eq.123"
}, {
returning: "id,status"
});
// ✅ Filters as object
// ✅ Body as object
// ✅ Type-safe options
```
### UPSERT Queries (PUT)
#### WASM API (Old)
```typescript
import { parseRequest } from 'postgrest-parser/wasm';
const result = parseRequest(
"PUT",
"users",
"email=eq.alice@example.com&returning=id,name",
JSON.stringify({ email: "alice@example.com", name: "Alice" }),
null
);
// ❌ Must manually construct filter for conflict detection
// ❌ Risk of mismatch between filter and body
```
#### Client API (New)
```typescript
import { createClient } from 'postgrest-parser';
const client = createClient();
const result = client.upsert("users", {
email: "alice@example.com",
name: "Alice"
}, ["email"], { // Conflict columns
returning: ["id", "name"]
});
// ✅ Auto-generates ON CONFLICT from conflict columns
// ✅ Type-safe, declarative API
// ✅ No risk of filter/body mismatch
```
### DELETE Queries
#### WASM API (Old)
```typescript
import { parseDelete } from 'postgrest-parser/wasm';
const result = parseDelete(
"users",
"status=eq.inactive&last_login=lt.2023-01-01",
null
);
// ❌ Filters as query string
```
#### Client API (New)
```typescript
import { createClient } from 'postgrest-parser';
const client = createClient();
const result = client.delete("users", {
status: "eq.inactive",
last_login: "lt.2023-01-01"
}, {
returning: "id"
});
// ✅ Filters as object
// ✅ Optional returning
```
### RPC Calls
#### WASM API (Old)
```typescript
import { parseRpc } from 'postgrest-parser/wasm';
const result = parseRpc(
"calculate_total",
JSON.stringify({ order_id: 123, tax_rate: 0.08 }),
"select=total,tax&limit=1",
null
);
// ❌ Args as JSON string
// ❌ Options as query string
```
#### Client API (New)
```typescript
import { createClient } from 'postgrest-parser';
const client = createClient();
const result = client.rpc("calculate_total", {
order_id: 123,
tax_rate: 0.08
}, {
select: ["total", "tax"],
limit: 1
});
// ✅ Native objects throughout
// ✅ Type-safe options
```
## Available Types
### Core Types
```typescript
import type {
SqlParam, // string | number | boolean | null | string[]
FilterOperator, // "eq" | "neq" | "gt" | "gte" | "lt" | ...
OrderDirection, // "asc" | "desc"
RequestHeaders, // { Prefer?: string; [key: string]: string }
} from 'postgrest-parser/types';
```
### Option Types
```typescript
import type {
SelectOptions, // { filters?, order?, limit?, offset?, count? }
InsertOptions, // { returning?, onConflict?, prefer? }
UpdateOptions, // { returning?, prefer? }
DeleteOptions, // { returning?, prefer? }
RpcOptions, // { select?, filters?, order?, limit?, offset? }
PreferOptions, // { return?, resolution?, missing?, count? }
} from 'postgrest-parser/types';
```
### Advanced Types
```typescript
import type {
Filter, // Single filter condition
LogicCondition, // AND/OR/NOT logic tree
OrderBy, // Order clause with direction and nulls position
ParsedQuery, // Complete parsed query structure
} from 'postgrest-parser/types';
```
## Integration Examples
### Express.js
```typescript
import express from 'express';
import { createClient } from 'postgrest-parser';
import { Pool } from 'pg';
import type { QueryResult } from 'postgrest-parser/types';
const app = express();
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const parser = createClient();
app.use(express.json());
app.get('/api/:table', async (req, res) => {
try {
const result: QueryResult = parser.select(req.params.table, {
filters: req.query as Record<string, string>,
limit: req.query.limit ? parseInt(req.query.limit as string) : undefined
});
const { rows } = await pool.query(result.query, result.params);
res.json(rows);
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
});
app.post('/api/:table', async (req, res) => {
try {
const result: QueryResult = parser.insert(req.params.table, req.body, {
returning: "*",
prefer: { return: "representation" }
});
const { rows } = await pool.query(result.query, result.params);
res.json(rows[0]);
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
});
```
### Next.js API Route
```typescript
// pages/api/users.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { createClient } from 'postgrest-parser';
import { query } from '@/lib/db';
import type { QueryResult } from 'postgrest-parser/types';
const parser = createClient();
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method === 'GET') {
const result: QueryResult = parser.select("users", {
filters: req.query as Record<string, string>,
limit: 10
});
const rows = await query(result.query, result.params);
res.json(rows);
} else if (req.method === 'POST') {
const result: QueryResult = parser.insert("users", req.body, {
returning: "*"
});
const rows = await query(result.query, result.params);
res.json(rows[0]);
} else {
res.status(405).json({ error: 'Method not allowed' });
}
}
```
### Supabase Edge Function
```typescript
// supabase/functions/users/index.ts
import { createClient } from '../_shared/postgrest-parser/client.ts';
import { createClient as createSupabaseClient } from '@supabase/supabase-js';
import type { QueryResult } from '../_shared/postgrest-parser/types.ts';
const supabase = createSupabaseClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
);
const parser = createClient();
Deno.serve(async (req) => {
const url = new URL(req.url);
const filters = Object.fromEntries(url.searchParams);
const result: QueryResult = parser.select("users", {
filters,
limit: 10
});
// Execute via Supabase
const { data, error } = await supabase.rpc('execute_sql', {
query: result.query,
params: result.params
});
if (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
headers: { 'Content-Type': 'application/json' }
});
}
return new Response(JSON.stringify(data), {
headers: { 'Content-Type': 'application/json' }
});
});
```
### Custom Type-Safe Wrapper
```typescript
import { createClient } from 'postgrest-parser';
import { Pool } from 'pg';
import type { QueryResult } from 'postgrest-parser/types';
export class Database {
private parser = createClient();
private pool: Pool;
constructor(connectionString: string) {
this.pool = new Pool({ connectionString });
}
async select<T = any>(
table: string,
options: Parameters<typeof this.parser.select>[1] = {}
): Promise<T[]> {
const result: QueryResult = this.parser.select(table, options);
const { rows } = await this.pool.query<T>(result.query, result.params);
return rows;
}
async insert<T = any>(
table: string,
data: Record<string, unknown> | Record<string, unknown>[],
options: Parameters<typeof this.parser.insert>[2] = {}
): Promise<T[]> {
const result: QueryResult = this.parser.insert(table, data, options);
const { rows } = await this.pool.query<T>(result.query, result.params);
return rows;
}
async update<T = any>(
table: string,
data: Record<string, unknown>,
filters: Record<string, string>,
options: Parameters<typeof this.parser.update>[3] = {}
): Promise<T[]> {
const result: QueryResult = this.parser.update(table, data, filters, options);
const { rows } = await this.pool.query<T>(result.query, result.params);
return rows;
}
async delete<T = any>(
table: string,
filters: Record<string, string>,
options: Parameters<typeof this.parser.delete>[2] = {}
): Promise<T[]> {
const result: QueryResult = this.parser.delete(table, filters, options);
const { rows } = await this.pool.query<T>(result.query, result.params);
return rows;
}
async rpc<T = any>(
functionName: string,
args: Record<string, unknown> = {},
options: Parameters<typeof this.parser.rpc>[2] = {}
): Promise<T[]> {
const result: QueryResult = this.parser.rpc(functionName, args, options);
const { rows } = await this.pool.query<T>(result.query, result.params);
return rows;
}
}
// Usage
const db = new Database(process.env.DATABASE_URL!);
interface User {
id: number;
name: string;
email: string;
status: string;
}
const users = await db.select<User>("users", {
filters: { status: "eq.active" },
limit: 10
});
// users is typed as User[]
```
## Best Practices
### 1. Use the Type-Safe Client
```typescript
// ✅ Recommended
import { createClient } from 'postgrest-parser';
const client = createClient();
// ❌ Avoid (unless you need low-level control)
import { parseQueryString } from 'postgrest-parser/wasm';
```
### 2. Leverage Type Inference
```typescript
const result = client.select("users", {
filters: { age: "gte.18" }
});
// result is automatically typed as QueryResult
```
### 3. Use Type Imports
```typescript
import type { QueryResult, SelectOptions } from 'postgrest-parser/types';
function buildQuery(options: SelectOptions): QueryResult {
return client.select("users", options);
}
```
### 4. Handle Errors Properly
```typescript
try {
const result = client.select("users", {
filters: { age: "invalid" }
});
const rows = await db.query(result.query, result.params);
} catch (error) {
if (error instanceof Error) {
console.error('Parse error:', error.message);
}
}
```
### 5. Reuse Client Instance
```typescript
// ✅ Create once, reuse
const client = createClient();
export function getUsers() {
return client.select("users", { ... });
}
export function createUser(data) {
return client.insert("users", data, { ... });
}
```
## Migration Guide
### From WASM API to Client API
```typescript
// Before (WASM API)
import { parseQueryString, parseInsert } from 'postgrest-parser/wasm';
const selectResult = parseQueryString(
"users",
"age=gte.18&order=created_at.desc&limit=10"
);
const insertResult = parseInsert(
"users",
JSON.stringify({ name: "Alice" }),
"returning=id",
JSON.stringify({ Prefer: "return=representation" })
);
// After (Client API)
import { createClient } from 'postgrest-parser';
const client = createClient();
const selectResult = client.select("users", {
filters: { age: "gte.18" },
order: ["created_at.desc"],
limit: 10
});
const insertResult = client.insert("users", {
name: "Alice"
}, {
returning: "id",
prefer: { return: "representation" }
});
```
## Type Safety Benefits
1. **No `any` Types**: All return values are properly typed
2. **IntelliSense Support**: Full autocomplete for all options
3. **Compile-Time Validation**: Catch errors before runtime
4. **Refactoring Safety**: TypeScript will catch breaking changes
5. **Self-Documenting**: Types serve as inline documentation
6. **Better DX**: Less time debugging, more time building
## Performance
The type-safe client is a thin wrapper around the WASM bindings with **zero runtime overhead**:
- No additional parsing or validation
- Direct pass-through to WASM functions
- Type checking happens at compile time only
- Same performance as using WASM API directly
## Summary
| Type Safety | ❌ Many `any` types | ✅ Fully typed |
| API Style | JSON strings | ✅ Native objects |
| HTTP Methods | Any string | ✅ Strict union type |
| Headers | JSON string | ✅ Typed object |
| IntelliSense | ❌ Limited | ✅ Full support |
| Error Messages | ❌ Generic | ✅ Detailed |
| Bundle Size | Smaller | +~2KB (minified) |
| Performance | Fast | ✅ Same (zero overhead) |
## Conclusion
The type-safe client provides a significantly better developer experience while maintaining the same performance as the low-level WASM API. Use it for all new code.
For more examples, see:
- [examples/typescript_client_example.ts](examples/typescript_client_example.ts)
- [examples/wasm_mutations_example.ts](examples/wasm_mutations_example.ts)