documenso-sdk-patterns
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseDocumenso SDK Patterns
Documenso SDK 模式
Overview
概述
Production-ready patterns for Documenso SDK usage in TypeScript and Python.
适用于TypeScript和Python的Documenso SDK生产就绪使用模式。
Prerequisites
前置条件
- Completed setup
documenso-install-auth - Familiarity with async/await patterns
- Understanding of error handling best practices
- 已完成设置
documenso-install-auth - 熟悉async/await模式
- 了解错误处理最佳实践
Instructions
操作步骤
Step 1: Singleton Client Pattern
步骤1:单例客户端模式
typescript
// src/documenso/client.ts
import { Documenso } from "@documenso/sdk-typescript";
let instance: Documenso | null = null;
export interface DocumensoClientConfig {
apiKey?: string;
baseUrl?: string;
timeout?: number;
}
export function getDocumensoClient(config?: DocumensoClientConfig): Documenso {
if (!instance) {
const apiKey = config?.apiKey ?? process.env.DOCUMENSO_API_KEY;
if (!apiKey) {
throw new Error(
"DOCUMENSO_API_KEY environment variable is required"
);
}
instance = new Documenso({
apiKey,
serverURL: config?.baseUrl ?? process.env.DOCUMENSO_BASE_URL,
timeoutMs: config?.timeout ?? 30000,
});
}
return instance;
}
// For testing - allows resetting the singleton
export function resetDocumensoClient(): void {
instance = null;
}typescript
// src/documenso/client.ts
import { Documenso } from "@documenso/sdk-typescript";
let instance: Documenso | null = null;
export interface DocumensoClientConfig {
apiKey?: string;
baseUrl?: string;
timeout?: number;
}
export function getDocumensoClient(config?: DocumensoClientConfig): Documenso {
if (!instance) {
const apiKey = config?.apiKey ?? process.env.DOCUMENSO_API_KEY;
if (!apiKey) {
throw new Error(
"DOCUMENSO_API_KEY environment variable is required"
);
}
instance = new Documenso({
apiKey,
serverURL: config?.baseUrl ?? process.env.DOCUMENSO_BASE_URL,
timeoutMs: config?.timeout ?? 30000,
});
}
return instance;
}
// For testing - allows resetting the singleton
export function resetDocumensoClient(): void {
instance = null;
}Step 2: Type-Safe Error Handling
步骤2:类型安全的错误处理
typescript
// src/documenso/errors.ts
import { SDKError } from "@documenso/sdk-typescript";
export class DocumensoError extends Error {
constructor(
message: string,
public readonly code: string,
public readonly statusCode: number,
public readonly retryable: boolean,
public readonly originalError?: Error
) {
super(message);
this.name = "DocumensoError";
}
}
export function wrapDocumensoError(error: unknown): DocumensoError {
if (error instanceof SDKError) {
const retryable = error.statusCode >= 500 || error.statusCode === 429;
return new DocumensoError(
error.message,
`DOCUMENSO_${error.statusCode}`,
error.statusCode,
retryable,
error
);
}
if (error instanceof Error) {
return new DocumensoError(
error.message,
"DOCUMENSO_UNKNOWN",
0,
false,
error
);
}
return new DocumensoError(
String(error),
"DOCUMENSO_UNKNOWN",
0,
false
);
}
// Safe wrapper for API calls
export async function safeDocumensoCall<T>(
operation: () => Promise<T>
): Promise<{ data: T | null; error: DocumensoError | null }> {
try {
const data = await operation();
return { data, error: null };
} catch (err) {
const error = wrapDocumensoError(err);
console.error({
code: error.code,
message: error.message,
statusCode: error.statusCode,
retryable: error.retryable,
});
return { data: null, error };
}
}typescript
// src/documenso/errors.ts
import { SDKError } from "@documenso/sdk-typescript";
export class DocumensoError extends Error {
constructor(
message: string,
public readonly code: string,
public readonly statusCode: number,
public readonly retryable: boolean,
public readonly originalError?: Error
) {
super(message);
this.name = "DocumensoError";
}
}
export function wrapDocumensoError(error: unknown): DocumensoError {
if (error instanceof SDKError) {
const retryable = error.statusCode >= 500 || error.statusCode === 429;
return new DocumensoError(
error.message,
`DOCUMENSO_${error.statusCode}`,
error.statusCode,
retryable,
error
);
}
if (error instanceof Error) {
return new DocumensoError(
error.message,
"DOCUMENSO_UNKNOWN",
0,
false,
error
);
}
return new DocumensoError(
String(error),
"DOCUMENSO_UNKNOWN",
0,
false
);
}
// Safe wrapper for API calls
export async function safeDocumensoCall<T>(
operation: () => Promise<T>
): Promise<{ data: T | null; error: DocumensoError | null }> {
try {
const data = await operation();
return { data, error: null };
} catch (err) {
const error = wrapDocumensoError(err);
console.error({
code: error.code,
message: error.message,
statusCode: error.statusCode,
retryable: error.retryable,
});
return { data: null, error };
}
}Step 3: Retry Logic with Exponential Backoff
步骤3:带指数退避的重试逻辑
typescript
// src/documenso/retry.ts
interface RetryConfig {
maxRetries?: number;
baseDelayMs?: number;
maxDelayMs?: number;
jitterMs?: number;
}
const DEFAULT_RETRY_CONFIG: Required<RetryConfig> = {
maxRetries: 3,
baseDelayMs: 1000,
maxDelayMs: 30000,
jitterMs: 500,
};
export async function withRetry<T>(
operation: () => Promise<T>,
config: RetryConfig = {}
): Promise<T> {
const { maxRetries, baseDelayMs, maxDelayMs, jitterMs } = {
...DEFAULT_RETRY_CONFIG,
...config,
};
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error: any) {
const statusCode = error.statusCode ?? error.status ?? 0;
// Only retry on 429 (rate limit) and 5xx (server errors)
const isRetryable = statusCode === 429 || statusCode >= 500;
if (attempt === maxRetries || !isRetryable) {
throw error;
}
const exponentialDelay = baseDelayMs * Math.pow(2, attempt);
const jitter = Math.random() * jitterMs;
const delay = Math.min(exponentialDelay + jitter, maxDelayMs);
console.log(
`Attempt ${attempt + 1} failed. Retrying in ${delay.toFixed(0)}ms...`
);
await new Promise((r) => setTimeout(r, delay));
}
}
throw new Error("Unreachable");
}typescript
// src/documenso/retry.ts
interface RetryConfig {
maxRetries?: number;
baseDelayMs?: number;
maxDelayMs?: number;
jitterMs?: number;
}
const DEFAULT_RETRY_CONFIG: Required<RetryConfig> = {
maxRetries: 3,
baseDelayMs: 1000,
maxDelayMs: 30000,
jitterMs: 500,
};
export async function withRetry<T>(
operation: () => Promise<T>,
config: RetryConfig = {}
): Promise<T> {
const { maxRetries, baseDelayMs, maxDelayMs, jitterMs } = {
...DEFAULT_RETRY_CONFIG,
...config,
};
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error: any) {
const statusCode = error.statusCode ?? error.status ?? 0;
// Only retry on 429 (rate limit) and 5xx (server errors)
const isRetryable = statusCode === 429 || statusCode >= 500;
if (attempt === maxRetries || !isRetryable) {
throw error;
}
const exponentialDelay = baseDelayMs * Math.pow(2, attempt);
const jitter = Math.random() * jitterMs;
const delay = Math.min(exponentialDelay + jitter, maxDelayMs);
console.log(
`Attempt ${attempt + 1} failed. Retrying in ${delay.toFixed(0)}ms...`
);
await new Promise((r) => setTimeout(r, delay));
}
}
throw new Error("Unreachable");
}Step 4: Document Service Facade
步骤4:文档服务外观
typescript
// src/services/document-service.ts
import { getDocumensoClient } from "../documenso/client";
import { withRetry } from "../documenso/retry";
import { safeDocumensoCall, DocumensoError } from "../documenso/errors";
export interface CreateDocumentInput {
title: string;
file?: Blob;
recipients: Array<{
email: string;
name: string;
role?: "SIGNER" | "VIEWER" | "APPROVER";
}>;
fields?: Array<{
recipientIndex: number;
type: "SIGNATURE" | "INITIALS" | "NAME" | "EMAIL" | "DATE" | "TEXT";
page: number;
x: number;
y: number;
width?: number;
height?: number;
}>;
sendImmediately?: boolean;
}
export interface DocumentResult {
documentId: string;
status: string;
recipients: Array<{ id: string; email: string }>;
}
export class DocumentService {
private client = getDocumensoClient();
async createDocument(input: CreateDocumentInput): Promise<DocumentResult> {
// Step 1: Create document
const doc = await withRetry(() =>
this.client.documents.createV0({
title: input.title,
file: input.file,
})
);
const documentId = doc.documentId!;
const recipientIds: Array<{ id: string; email: string }> = [];
// Step 2: Add recipients
for (const recipient of input.recipients) {
const result = await withRetry(() =>
this.client.documentsRecipients.createV0({
documentId,
email: recipient.email,
name: recipient.name,
role: recipient.role ?? "SIGNER",
})
);
recipientIds.push({
id: result.recipientId!,
email: recipient.email,
});
}
// Step 3: Add fields
if (input.fields) {
for (const field of input.fields) {
const recipientId = recipientIds[field.recipientIndex]?.id;
if (!recipientId) continue;
await withRetry(() =>
this.client.documentsFields.createV0({
documentId,
recipientId,
type: field.type,
page: field.page,
positionX: field.x,
positionY: field.y,
width: field.width ?? 200,
height: field.height ?? 60,
})
);
}
}
// Step 4: Send if requested
if (input.sendImmediately) {
await withRetry(() =>
this.client.documents.sendV0({ documentId })
);
}
return {
documentId,
status: input.sendImmediately ? "SENT" : "DRAFT",
recipients: recipientIds,
};
}
async getDocument(documentId: string): Promise<DocumentResult | null> {
const { data, error } = await safeDocumensoCall(() =>
this.client.documents.getV0({ documentId })
);
if (error) {
if (error.statusCode === 404) return null;
throw error;
}
return {
documentId: data!.id!,
status: data!.status!,
recipients:
data!.recipients?.map((r) => ({
id: r.id!,
email: r.email!,
})) ?? [],
};
}
async deleteDocument(documentId: string): Promise<boolean> {
const { error } = await safeDocumensoCall(() =>
this.client.documents.deleteV0({ documentId })
);
return error === null;
}
}
// Singleton instance
let documentService: DocumentService | null = null;
export function getDocumentService(): DocumentService {
if (!documentService) {
documentService = new DocumentService();
}
return documentService;
}typescript
// src/services/document-service.ts
import { getDocumensoClient } from "../documenso/client";
import { withRetry } from "../documenso/retry";
import { safeDocumensoCall, DocumensoError } from "../documenso/errors";
export interface CreateDocumentInput {
title: string;
file?: Blob;
recipients: Array<{
email: string;
name: string;
role?: "SIGNER" | "VIEWER" | "APPROVER";
}>;
fields?: Array<{
recipientIndex: number;
type: "SIGNATURE" | "INITIALS" | "NAME" | "EMAIL" | "DATE" | "TEXT";
page: number;
x: number;
y: number;
width?: number;
height?: number;
}>;
sendImmediately?: boolean;
}
export interface DocumentResult {
documentId: string;
status: string;
recipients: Array<{ id: string; email: string }>;
}
export class DocumentService {
private client = getDocumensoClient();
async createDocument(input: CreateDocumentInput): Promise<DocumentResult> {
// Step 1: Create document
const doc = await withRetry(() =>
this.client.documents.createV0({
title: input.title,
file: input.file,
})
);
const documentId = doc.documentId!;
const recipientIds: Array<{ id: string; email: string }> = [];
// Step 2: Add recipients
for (const recipient of input.recipients) {
const result = await withRetry(() =>
this.client.documentsRecipients.createV0({
documentId,
email: recipient.email,
name: recipient.name,
role: recipient.role ?? "SIGNER",
})
);
recipientIds.push({
id: result.recipientId!,
email: recipient.email,
});
}
// Step 3: Add fields
if (input.fields) {
for (const field of input.fields) {
const recipientId = recipientIds[field.recipientIndex]?.id;
if (!recipientId) continue;
await withRetry(() =>
this.client.documentsFields.createV0({
documentId,
recipientId,
type: field.type,
page: field.page,
positionX: field.x,
positionY: field.y,
width: field.width ?? 200,
height: field.height ?? 60,
})
);
}
}
// Step 4: Send if requested
if (input.sendImmediately) {
await withRetry(() =>
this.client.documents.sendV0({ documentId })
);
}
return {
documentId,
status: input.sendImmediately ? "SENT" : "DRAFT",
recipients: recipientIds,
};
}
async getDocument(documentId: string): Promise<DocumentResult | null> {
const { data, error } = await safeDocumensoCall(() =>
this.client.documents.getV0({ documentId })
);
if (error) {
if (error.statusCode === 404) return null;
throw error;
}
return {
documentId: data!.id!,
status: data!.status!,
recipients:
data!.recipients?.map((r) => ({
id: r.id!,
email: r.email!,
})) ?? [],
};
}
async deleteDocument(documentId: string): Promise<boolean> {
const { error } = await safeDocumensoCall(() =>
this.client.documents.deleteV0({ documentId })
);
return error === null;
}
}
// Singleton instance
let documentService: DocumentService | null = null;
export function getDocumentService(): DocumentService {
if (!documentService) {
documentService = new DocumentService();
}
return documentService;
}Step 5: Response Validation with Zod
步骤5:使用Zod进行响应验证
typescript
// src/documenso/types.ts
import { z } from "zod";
export const DocumentStatusSchema = z.enum([
"DRAFT",
"PENDING",
"COMPLETED",
"REJECTED",
"CANCELLED",
]);
export const RecipientSchema = z.object({
id: z.string(),
email: z.string().email(),
name: z.string(),
role: z.enum(["SIGNER", "VIEWER", "APPROVER", "CC"]),
signingStatus: z.enum(["NOT_SIGNED", "SIGNED", "REJECTED"]).optional(),
});
export const DocumentSchema = z.object({
id: z.string(),
title: z.string(),
status: DocumentStatusSchema,
createdAt: z.string().datetime(),
updatedAt: z.string().datetime(),
recipients: z.array(RecipientSchema).optional(),
});
export type Document = z.infer<typeof DocumentSchema>;
export type DocumentStatus = z.infer<typeof DocumentStatusSchema>;
// Validate API response
export function validateDocument(data: unknown): Document {
return DocumentSchema.parse(data);
}typescript
// src/documenso/types.ts
import { z } from "zod";
export const DocumentStatusSchema = z.enum([
"DRAFT",
"PENDING",
"COMPLETED",
"REJECTED",
"CANCELLED",
]);
export const RecipientSchema = z.object({
id: z.string(),
email: z.string().email(),
name: z.string(),
role: z.enum(["SIGNER", "VIEWER", "APPROVER", "CC"]),
signingStatus: z.enum(["NOT_SIGNED", "SIGNED", "REJECTED"]).optional(),
});
export const DocumentSchema = z.object({
id: z.string(),
title: z.string(),
status: DocumentStatusSchema,
createdAt: z.string().datetime(),
updatedAt: z.string().datetime(),
recipients: z.array(RecipientSchema).optional(),
});
export type Document = z.infer<typeof DocumentSchema>;
export type DocumentStatus = z.infer<typeof DocumentStatusSchema>;
// Validate API response
export function validateDocument(data: unknown): Document {
return DocumentSchema.parse(data);
}Python Patterns
Python模式
python
undefinedpython
undefinedsrc/documenso/client.py
src/documenso/client.py
import os
from typing import Optional
from functools import lru_cache
from documenso_sdk import Documenso
@lru_cache(maxsize=1)
def get_documenso_client(
api_key: Optional[str] = None,
base_url: Optional[str] = None,
) -> Documenso:
"""Get singleton Documenso client."""
key = api_key or os.environ.get("DOCUMENSO_API_KEY")
if not key:
raise ValueError("DOCUMENSO_API_KEY is required")
return Documenso(
api_key=key,
server_url=base_url or os.environ.get("DOCUMENSO_BASE_URL"),
)import os
from typing import Optional
from functools import lru_cache
from documenso_sdk import Documenso
@lru_cache(maxsize=1)
def get_documenso_client(
api_key: Optional[str] = None,
base_url: Optional[str] = None,
) -> Documenso:
"""Get singleton Documenso client."""
key = api_key or os.environ.get("DOCUMENSO_API_KEY")
if not key:
raise ValueError("DOCUMENSO_API_KEY is required")
return Documenso(
api_key=key,
server_url=base_url or os.environ.get("DOCUMENSO_BASE_URL"),
)src/documenso/retry.py
src/documenso/retry.py
import asyncio
import random
from typing import TypeVar, Callable, Awaitable
T = TypeVar("T")
async def with_retry(
operation: Callable[[], Awaitable[T]],
max_retries: int = 3,
base_delay: float = 1.0,
max_delay: float = 30.0,
) -> T:
"""Execute operation with exponential backoff retry."""
for attempt in range(max_retries + 1):
try:
return await operation()
except Exception as e:
status = getattr(e, "status_code", 0)
is_retryable = status == 429 or status >= 500
if attempt == max_retries or not is_retryable:
raise
delay = min(base_delay * (2 ** attempt) + random.uniform(0, 0.5), max_delay)
print(f"Attempt {attempt + 1} failed. Retrying in {delay:.1f}s...")
await asyncio.sleep(delay)
raise RuntimeError("Unreachable")undefinedimport asyncio
import random
from typing import TypeVar, Callable, Awaitable
T = TypeVar("T")
async def with_retry(
operation: Callable[[], Awaitable[T]],
max_retries: int = 3,
base_delay: float = 1.0,
max_delay: float = 30.0,
) -> T:
"""Execute operation with exponential backoff retry."""
for attempt in range(max_retries + 1):
try:
return await operation()
except Exception as e:
status = getattr(e, "status_code", 0)
is_retryable = status == 429 or status >= 500
if attempt == max_retries or not is_retryable:
raise
delay = min(base_delay * (2 ** attempt) + random.uniform(0, 0.5), max_delay)
print(f"Attempt {attempt + 1} failed. Retrying in {delay:.1f}s...")
await asyncio.sleep(delay)
raise RuntimeError("Unreachable")undefinedOutput
输出
- Type-safe client singleton
- Robust error handling with retryable classification
- Automatic retry with exponential backoff
- Service facade for common operations
- Runtime validation for API responses
- 类型安全的单例客户端
- 带有可重试分类的健壮错误处理
- 带指数退避的自动重试
- 用于常见操作的服务外观
- API响应的运行时验证
Error Handling Patterns
错误处理模式
| Pattern | Use Case | Benefit |
|---|---|---|
| Safe wrapper | All API calls | Prevents uncaught exceptions |
| Retry logic | Transient failures | Improves reliability |
| Type guards | Response validation | Catches API changes |
| Service facade | Complex workflows | Encapsulates multi-step operations |
| 模式 | 使用场景 | 优势 |
|---|---|---|
| 安全包装器 | 所有API调用 | 防止未捕获异常 |
| 重试逻辑 | 临时故障 | 提升可靠性 |
| 类型守卫 | 响应验证 | 捕获API变更 |
| 服务外观 | 复杂工作流 | 封装多步骤操作 |
Resources
资源
Next Steps
下一步
Apply patterns in for document creation workflows.
documenso-core-workflow-a在中应用这些模式以实现文档创建工作流。
documenso-core-workflow-a