返回 Skill 列表
extension
分类: 开发与工程无需 API Key

documenso-reference-architecture

实现Documenso参考架构,采用最佳实践项目布局。在设计新的Documenso集成、审查项目结构或为文档签名应用程序建立架构标准时使用。可以通过诸如“documenso架构”、“documenso最佳实践”、“documenso项目结构”、“如何组织documenso”等短语触发。

person作者: jakexiaohubgithub

Documenso Reference Architecture

Overview

Production-ready architecture patterns for Documenso document signing integrations.

Prerequisites

  • Understanding of layered architecture
  • Documenso SDK knowledge
  • TypeScript project setup
  • Testing framework configured

Project Structure

my-signing-app/
├── src/
│   ├── documenso/
│   │   ├── client.ts           # Singleton client wrapper
│   │   ├── config.ts           # Environment configuration
│   │   ├── types.ts            # TypeScript types
│   │   ├── errors.ts           # Custom error classes
│   │   └── handlers/
│   │       ├── webhooks.ts     # Webhook handlers
│   │       └── events.ts       # Event processing
│   ├── services/
│   │   └── signing/
│   │       ├── index.ts        # Service facade
│   │       ├── documents.ts    # Document operations
│   │       ├── templates.ts    # Template operations
│   │       └── cache.ts        # Caching layer
│   ├── api/
│   │   └── signing/
│   │       ├── routes.ts       # API routes
│   │       └── webhook.ts      # Webhook endpoint
│   ├── jobs/
│   │   └── signing/
│   │       ├── cleanup.ts      # Draft cleanup job
│   │       └── sync.ts         # Status sync job
│   └── utils/
│       ├── pdf.ts              # PDF utilities
│       └── validation.ts       # Input validation
├── tests/
│   ├── unit/
│   │   └── signing/
│   │       ├── documents.test.ts
│   │       └── templates.test.ts
│   └── integration/
│       └── signing/
│           └── workflows.test.ts
├── config/
│   ├── documenso.development.json
│   ├── documenso.staging.json
│   └── documenso.production.json
├── templates/
│   └── pdf/                    # PDF template files
│       ├── nda.pdf
│       ├── contract.pdf
│       └── agreement.pdf
└── docs/
    └── signing/
        ├── SETUP.md
        └── RUNBOOK.md

Layer Architecture

┌─────────────────────────────────────────┐
│             API Layer                    │
│   (Controllers, Routes, Webhooks)        │
├─────────────────────────────────────────┤
│           Service Layer                  │
│  (Business Logic, Orchestration)         │
├─────────────────────────────────────────┤
│          Documenso Layer                 │
│   (Client, Types, Error Handling)        │
├─────────────────────────────────────────┤
│         Infrastructure Layer             │
│    (Cache, Queue, Monitoring)            │
└─────────────────────────────────────────┘

Key Components

Documenso Client Wrapper

// src/documenso/client.ts
import { Documenso } from "@documenso/sdk-typescript";
import { DocumensoConfig, loadDocumensoConfig } from "./config";
import { CacheService } from "../services/signing/cache";
import { MetricsService } from "../utils/metrics";

export class DocumensoService {
  private client: Documenso;
  private cache: CacheService;
  private metrics: MetricsService;

  constructor(config: DocumensoConfig) {
    this.client = new Documenso({
      apiKey: config.apiKey,
      serverURL: config.baseUrl,
      timeoutMs: config.timeout,
    });
    this.cache = new CacheService(config.cacheOptions);
    this.metrics = new MetricsService("documenso");
  }

  async getDocument(documentId: string) {
    return this.cache.getOrFetch(
      `doc:${documentId}`,
      () => this.metrics.track("getDocument", () =>
        this.client.documents.getV0({ documentId })
      ),
      { ttl: 300 }
    );
  }

  async createDocument(input: CreateDocumentInput) {
    return this.metrics.track("createDocument", () =>
      this.client.documents.createV0(input)
    );
  }

  // Expose raw client for advanced use cases
  getRawClient(): Documenso {
    return this.client;
  }
}

// Singleton instance
let service: DocumensoService | null = null;

export function getDocumensoService(): DocumensoService {
  if (!service) {
    const config = loadDocumensoConfig();
    service = new DocumensoService(config);
  }
  return service;
}

Configuration Management

// src/documenso/config.ts
import { z } from "zod";

const ConfigSchema = z.object({
  apiKey: z.string().min(1),
  baseUrl: z.string().url().optional(),
  timeout: z.number().default(30000),
  cacheOptions: z.object({
    enabled: z.boolean().default(true),
    ttl: z.number().default(300),
  }).default({}),
});

export type DocumensoConfig = z.infer<typeof ConfigSchema>;

export function loadDocumensoConfig(): DocumensoConfig {
  const env = process.env.NODE_ENV ?? "development";

  // Load environment-specific config
  let fileConfig = {};
  try {
    fileConfig = require(`../../config/documenso.${env}.json`);
  } catch {
    console.warn(`No config file for environment: ${env}`);
  }

  // Merge with environment variables
  const config = {
    apiKey: process.env.DOCUMENSO_API_KEY ?? "",
    baseUrl: process.env.DOCUMENSO_BASE_URL,
    ...fileConfig,
  };

  return ConfigSchema.parse(config);
}

Error Boundary

// src/documenso/errors.ts
export class DocumensoServiceError 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 = "DocumensoServiceError";
  }

  static fromSdkError(error: any): DocumensoServiceError {
    const statusCode = error.statusCode ?? error.status ?? 0;
    const retryable = statusCode === 429 || statusCode >= 500;

    return new DocumensoServiceError(
      error.message,
      `DOCUMENSO_${statusCode}`,
      statusCode,
      retryable,
      error
    );
  }
}

export async function withErrorHandling<T>(
  operation: () => Promise<T>
): Promise<T> {
  try {
    return await operation();
  } catch (error) {
    throw DocumensoServiceError.fromSdkError(error);
  }
}

Service Facade

// src/services/signing/index.ts
import { getDocumensoService } from "../../documenso/client";
import { withErrorHandling } from "../../documenso/errors";
import { validateCreateDocumentInput } from "../../utils/validation";

export interface SigningService {
  createAndSendDocument(input: CreateDocumentInput): Promise<DocumentResult>;
  getDocumentStatus(documentId: string): Promise<DocumentStatus>;
  downloadSignedDocument(documentId: string): Promise<Buffer>;
}

export class SigningServiceImpl implements SigningService {
  private documenso = getDocumensoService();

  async createAndSendDocument(
    input: CreateDocumentInput
  ): Promise<DocumentResult> {
    // Validate input
    const validated = validateCreateDocumentInput(input);

    return withErrorHandling(async () => {
      // Create from template
      const envelope = await this.documenso.getRawClient().envelopes.useV0({
        templateId: validated.templateId,
        title: validated.title,
        recipients: validated.recipients.map((r, i) => ({
          email: r.email,
          name: r.name,
          signerIndex: i,
        })),
      });

      // Send immediately
      await this.documenso.getRawClient().envelopes.distributeV0({
        envelopeId: envelope.envelopeId!,
      });

      return {
        documentId: envelope.envelopeId!,
        status: "SENT",
      };
    });
  }

  async getDocumentStatus(documentId: string): Promise<DocumentStatus> {
    const doc = await this.documenso.getDocument(documentId);
    return {
      id: doc.id!,
      status: doc.status!,
      recipients: doc.recipients?.map((r) => ({
        email: r.email!,
        status: r.signingStatus!,
      })) ?? [],
    };
  }

  async downloadSignedDocument(documentId: string): Promise<Buffer> {
    return withErrorHandling(async () => {
      const result = await this.documenso.getRawClient().documents.downloadV0({
        documentId,
      });
      // Handle the download response
      return Buffer.from(result as any);
    });
  }
}

// Export singleton
let signingService: SigningService | null = null;

export function getSigningService(): SigningService {
  if (!signingService) {
    signingService = new SigningServiceImpl();
  }
  return signingService;
}

Health Check

// src/documenso/health.ts
import { getDocumensoService } from "./client";

export interface HealthStatus {
  status: "healthy" | "degraded" | "unhealthy";
  latencyMs: number;
  error?: string;
}

export async function checkDocumensoHealth(): Promise<HealthStatus> {
  const service = getDocumensoService();
  const start = Date.now();

  try {
    await service.getRawClient().documents.findV0({ perPage: 1 });
    return {
      status: "healthy",
      latencyMs: Date.now() - start,
    };
  } catch (error: any) {
    return {
      status: "unhealthy",
      latencyMs: Date.now() - start,
      error: error.message,
    };
  }
}

Data Flow Diagram

User Request
     │
     ▼
┌─────────────┐
│   API       │
│   Layer     │
└──────┬──────┘
       │
       ▼
┌─────────────┐    ┌─────────────┐
│  Service    │───▶│   Cache     │
│   Layer     │    │  (Redis)    │
└──────┬──────┘    └─────────────┘
       │
       ▼
┌─────────────┐    ┌─────────────┐
│ Documenso   │───▶│   Queue     │
│   Layer     │    │  (Bull)     │
└──────┬──────┘    └─────────────┘
       │
       ▼
┌─────────────┐
│ Documenso   │
│   API       │
└─────────────┘

Webhook Architecture

Documenso
    │
    ▼ POST /webhooks/documenso
┌─────────────┐
│  Webhook    │
│  Handler    │
└──────┬──────┘
       │
       ▼
┌─────────────┐
│  Event      │
│  Queue      │
└──────┬──────┘
       │
       ├──▶ Update Database
       ├──▶ Send Notifications
       ├──▶ Trigger Workflows
       └──▶ Update Cache

Setup Script

#!/bin/bash
# scripts/setup-documenso-structure.sh

# Create directory structure
mkdir -p src/documenso/handlers
mkdir -p src/services/signing
mkdir -p src/api/signing
mkdir -p src/jobs/signing
mkdir -p src/utils
mkdir -p tests/unit/signing
mkdir -p tests/integration/signing
mkdir -p config
mkdir -p templates/pdf
mkdir -p docs/signing

# Create placeholder files
touch src/documenso/{client,config,types,errors}.ts
touch src/documenso/handlers/{webhooks,events}.ts
touch src/services/signing/{index,documents,templates,cache}.ts
touch src/api/signing/{routes,webhook}.ts
touch config/documenso.{development,staging,production}.json

echo "Documenso project structure created!"

Output

  • Structured project layout
  • Layered architecture implemented
  • Error handling configured
  • Health checks ready

Error Handling

| Issue | Cause | Solution | |-------|-------|----------| | Circular dependencies | Wrong layering | Separate by layer | | Config not loading | Wrong paths | Verify file locations | | Cache misses | Wrong keys | Check key generation | | Test isolation | Shared state | Use dependency injection |

Resources

Next Steps

For multi-environment setup, see documenso-multi-env-setup.