Files
Portfolio/blog-posts/94-developer-experience-dx.md
damjan_savicandClaude Opus 4.5 e1bbe5455d Initial commit: Portfolio Website
Vollständige Next.js 15 Portfolio-Website mit:
- Blog-System mit 100+ Artikeln
- Supabase-Integration
- Responsive Design mit Tailwind CSS
- TypeScript-Konfiguration
- Testing-Setup mit Vitest und Playwright

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 15:07:20 +01:00

20 KiB

Developer Experience (DX)

Meta-Description: Developer Experience für APIs optimieren. SDKs, CLI Tools, Interactive Docs und Developer Portals für bessere Adoption.

Keywords: Developer Experience, DX, SDK, CLI, API Documentation, Developer Portal, Onboarding, API Adoption


Einführung

Developer Experience (DX) entscheidet über API-Adoption. Gute SDKs, intuitive Dokumentation und hilfreiche Error Messages machen den Unterschied. Dieser Guide zeigt, wie man APIs developer-friendly gestaltet.


DX Overview

┌─────────────────────────────────────────────────────────────┐
│              DEVELOPER EXPERIENCE PILLARS                    │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  1. DOCUMENTATION:                                          │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  ├── Quick Start Guide (< 5 min to first call)     │   │
│  │  ├── Interactive API Reference (Try it!)           │   │
│  │  ├── Code Examples (Copy-paste ready)              │   │
│  │  ├── Tutorials & Guides                            │   │
│  │  └── Changelog & Migration Guides                   │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  2. SDKs & TOOLS:                                          │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  ├── Official SDKs (JS, Python, Go, etc.)          │   │
│  │  ├── CLI Tool                                       │   │
│  │  ├── Postman/Insomnia Collection                   │   │
│  │  ├── VS Code Extension                             │   │
│  │  └── Testing Tools                                  │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  3. ERROR HANDLING:                                        │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  ├── Clear error messages                          │   │
│  │  ├── Error codes & documentation                   │   │
│  │  ├── Suggested fixes                               │   │
│  │  └── Stack traces in dev mode                      │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  4. DEVELOPER PORTAL:                                      │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  ├── API Key Management                            │   │
│  │  ├── Usage Dashboard                               │   │
│  │  ├── Webhook Management                            │   │
│  │  └── Support & Community                           │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  Time to First API Call:                                    │
│  ├── Excellent: < 5 minutes                                │
│  ├── Good: 5-15 minutes                                    │
│  ├── Needs Work: 15-30 minutes                             │
│  └── Poor: > 30 minutes                                    │
│                                                             │
└─────────────────────────────────────────────────────────────┘

TypeScript SDK

// sdk/src/client.ts
import { z } from 'zod';

interface ClientConfig {
  apiKey: string;
  baseUrl?: string;
  timeout?: number;
  retries?: number;
  onError?: (error: APIError) => void;
}

interface RequestOptions {
  method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
  path: string;
  body?: unknown;
  query?: Record<string, string | number | boolean | undefined>;
  headers?: Record<string, string>;
}

export class APIError extends Error {
  constructor(
    public code: string,
    message: string,
    public statusCode: number,
    public details?: unknown
  ) {
    super(message);
    this.name = 'APIError';
  }
}

export class MyAPIClient {
  private config: Required<ClientConfig>;

  // Resources
  public projects: ProjectsResource;
  public users: UsersResource;
  public webhooks: WebhooksResource;

  constructor(config: ClientConfig) {
    this.config = {
      apiKey: config.apiKey,
      baseUrl: config.baseUrl || 'https://api.example.com/v1',
      timeout: config.timeout || 30000,
      retries: config.retries || 3,
      onError: config.onError || (() => {})
    };

    // Initialize resources
    this.projects = new ProjectsResource(this);
    this.users = new UsersResource(this);
    this.webhooks = new WebhooksResource(this);
  }

  async request<T>(options: RequestOptions): Promise<T> {
    const url = new URL(options.path, this.config.baseUrl);

    // Add query parameters
    if (options.query) {
      Object.entries(options.query).forEach(([key, value]) => {
        if (value !== undefined) {
          url.searchParams.set(key, String(value));
        }
      });
    }

    const headers: Record<string, string> = {
      'Authorization': `Bearer ${this.config.apiKey}`,
      'Content-Type': 'application/json',
      'User-Agent': 'myapi-sdk-js/1.0.0',
      ...options.headers
    };

    let lastError: Error | null = null;

    for (let attempt = 0; attempt < this.config.retries; attempt++) {
      try {
        const response = await fetch(url.toString(), {
          method: options.method,
          headers,
          body: options.body ? JSON.stringify(options.body) : undefined,
          signal: AbortSignal.timeout(this.config.timeout)
        });

        const data = await response.json();

        if (!response.ok) {
          const error = new APIError(
            data.error?.code || 'UNKNOWN_ERROR',
            data.error?.message || 'An error occurred',
            response.status,
            data.error?.details
          );
          this.config.onError(error);
          throw error;
        }

        return data.data as T;
      } catch (error) {
        lastError = error as Error;

        // Don't retry client errors
        if (error instanceof APIError && error.statusCode < 500) {
          throw error;
        }

        // Wait before retry with exponential backoff
        if (attempt < this.config.retries - 1) {
          await new Promise(resolve =>
            setTimeout(resolve, Math.pow(2, attempt) * 1000)
          );
        }
      }
    }

    throw lastError;
  }
}

// Resource classes
class ProjectsResource {
  constructor(private client: MyAPIClient) {}

  async list(params?: {
    page?: number;
    limit?: number;
    status?: 'active' | 'archived';
  }): Promise<{ data: Project[]; pagination: Pagination }> {
    return this.client.request({
      method: 'GET',
      path: '/projects',
      query: params
    });
  }

  async create(data: CreateProjectInput): Promise<Project> {
    return this.client.request({
      method: 'POST',
      path: '/projects',
      body: data
    });
  }

  async get(id: string): Promise<Project> {
    return this.client.request({
      method: 'GET',
      path: `/projects/${id}`
    });
  }

  async update(id: string, data: UpdateProjectInput): Promise<Project> {
    return this.client.request({
      method: 'PATCH',
      path: `/projects/${id}`,
      body: data
    });
  }

  async delete(id: string): Promise<void> {
    return this.client.request({
      method: 'DELETE',
      path: `/projects/${id}`
    });
  }
}

// Usage example
const client = new MyAPIClient({
  apiKey: 'sk_live_...',
  onError: (error) => {
    console.error(`API Error: ${error.code} - ${error.message}`);
  }
});

const projects = await client.projects.list({ status: 'active' });
const newProject = await client.projects.create({ name: 'My Project' });

CLI Tool

// cli/src/index.ts
#!/usr/bin/env node
import { Command } from 'commander';
import chalk from 'chalk';
import ora from 'ora';
import inquirer from 'inquirer';
import { MyAPIClient } from '@myapi/sdk';

const program = new Command();

// Config management
const config = {
  getApiKey: () => process.env.MYAPI_KEY || loadConfig().apiKey,
  setApiKey: (key: string) => saveConfig({ apiKey: key })
};

program
  .name('myapi')
  .description('CLI tool for MyAPI')
  .version('1.0.0');

// Auth command
program
  .command('login')
  .description('Configure API key')
  .action(async () => {
    const { apiKey } = await inquirer.prompt([
      {
        type: 'password',
        name: 'apiKey',
        message: 'Enter your API key:',
        validate: (input) => input.length > 0 || 'API key is required'
      }
    ]);

    config.setApiKey(apiKey);
    console.log(chalk.green('✓ API key saved'));
  });

// Projects commands
const projects = program.command('projects').description('Manage projects');

projects
  .command('list')
  .description('List all projects')
  .option('-s, --status <status>', 'Filter by status')
  .option('--json', 'Output as JSON')
  .action(async (options) => {
    const spinner = ora('Fetching projects...').start();

    try {
      const client = new MyAPIClient({ apiKey: config.getApiKey() });
      const result = await client.projects.list({
        status: options.status
      });

      spinner.stop();

      if (options.json) {
        console.log(JSON.stringify(result.data, null, 2));
      } else {
        console.log(chalk.bold('\nProjects:\n'));
        result.data.forEach(project => {
          const status = project.status === 'active'
            ? chalk.green('●')
            : chalk.gray('○');
          console.log(`  ${status} ${project.name} (${project.id})`);
        });
        console.log(`\n  Total: ${result.pagination.total}`);
      }
    } catch (error) {
      spinner.fail('Failed to fetch projects');
      console.error(chalk.red(error.message));
      process.exit(1);
    }
  });

projects
  .command('create')
  .description('Create a new project')
  .option('-n, --name <name>', 'Project name')
  .option('-d, --description <description>', 'Project description')
  .action(async (options) => {
    let { name, description } = options;

    // Interactive mode if not provided
    if (!name) {
      const answers = await inquirer.prompt([
        {
          type: 'input',
          name: 'name',
          message: 'Project name:',
          validate: (input) => input.length > 0 || 'Name is required'
        },
        {
          type: 'input',
          name: 'description',
          message: 'Description (optional):'
        }
      ]);
      name = answers.name;
      description = answers.description;
    }

    const spinner = ora('Creating project...').start();

    try {
      const client = new MyAPIClient({ apiKey: config.getApiKey() });
      const project = await client.projects.create({ name, description });

      spinner.succeed('Project created');
      console.log(chalk.gray(`  ID: ${project.id}`));
    } catch (error) {
      spinner.fail('Failed to create project');
      console.error(chalk.red(error.message));
      process.exit(1);
    }
  });

// API request command for debugging
program
  .command('api <method> <path>')
  .description('Make a raw API request')
  .option('-d, --data <json>', 'Request body (JSON)')
  .option('-v, --verbose', 'Show full response')
  .action(async (method, path, options) => {
    const client = new MyAPIClient({ apiKey: config.getApiKey() });

    try {
      const response = await client.request({
        method: method.toUpperCase(),
        path,
        body: options.data ? JSON.parse(options.data) : undefined
      });

      console.log(JSON.stringify(response, null, 2));
    } catch (error) {
      console.error(chalk.red(error.message));
      if (options.verbose && error.details) {
        console.error(JSON.stringify(error.details, null, 2));
      }
      process.exit(1);
    }
  });

program.parse();

Error Messages

// lib/errors/messages.ts
interface ErrorDefinition {
  code: string;
  message: string;
  httpStatus: number;
  suggestion?: string;
  docsUrl?: string;
}

export const errors: Record<string, ErrorDefinition> = {
  AUTHENTICATION_FAILED: {
    code: 'AUTHENTICATION_FAILED',
    message: 'Invalid API key provided',
    httpStatus: 401,
    suggestion: 'Check that your API key is correct and has not expired. You can find your API keys in the dashboard.',
    docsUrl: '/docs/authentication'
  },
  INSUFFICIENT_PERMISSIONS: {
    code: 'INSUFFICIENT_PERMISSIONS',
    message: 'Your API key does not have permission for this action',
    httpStatus: 403,
    suggestion: 'Ensure your API key has the required scopes. You may need to generate a new key with additional permissions.',
    docsUrl: '/docs/api-keys#scopes'
  },
  RESOURCE_NOT_FOUND: {
    code: 'RESOURCE_NOT_FOUND',
    message: 'The requested resource was not found',
    httpStatus: 404,
    suggestion: 'Verify the resource ID is correct and that you have access to this resource.'
  },
  VALIDATION_ERROR: {
    code: 'VALIDATION_ERROR',
    message: 'The request body contains invalid data',
    httpStatus: 422,
    suggestion: 'Check the error details for specific field errors.',
    docsUrl: '/docs/error-handling#validation'
  },
  RATE_LIMIT_EXCEEDED: {
    code: 'RATE_LIMIT_EXCEEDED',
    message: 'Too many requests',
    httpStatus: 429,
    suggestion: 'Wait before retrying. Check the Retry-After header. Consider upgrading your plan for higher limits.',
    docsUrl: '/docs/rate-limits'
  },
  INTERNAL_ERROR: {
    code: 'INTERNAL_ERROR',
    message: 'An internal error occurred',
    httpStatus: 500,
    suggestion: 'This is likely a temporary issue. Please try again. If the problem persists, contact support with the request ID.'
  }
};

export function createErrorResponse(
  errorCode: string,
  details?: unknown,
  requestId?: string
) {
  const errorDef = errors[errorCode] || errors.INTERNAL_ERROR;

  return {
    error: {
      code: errorDef.code,
      message: errorDef.message,
      suggestion: errorDef.suggestion,
      docsUrl: errorDef.docsUrl ? `https://example.com${errorDef.docsUrl}` : undefined,
      details,
      requestId
    }
  };
}

// Example error response:
// {
//   "error": {
//     "code": "VALIDATION_ERROR",
//     "message": "The request body contains invalid data",
//     "suggestion": "Check the error details for specific field errors.",
//     "docsUrl": "https://example.com/docs/error-handling#validation",
//     "details": {
//       "errors": [
//         { "field": "name", "message": "Name is required" },
//         { "field": "email", "message": "Invalid email format" }
//       ]
//     },
//     "requestId": "req_abc123"
//   }
// }

Interactive Code Examples

// components/CodeExample.tsx
'use client';

import { useState } from 'react';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';

interface CodeExampleProps {
  examples: {
    language: string;
    label: string;
    code: string;
  }[];
  endpoint: string;
  method: string;
}

export function CodeExample({ examples, endpoint, method }: CodeExampleProps) {
  const [selectedLang, setSelectedLang] = useState(examples[0].language);
  const [response, setResponse] = useState<string | null>(null);
  const [loading, setLoading] = useState(false);

  const currentExample = examples.find(e => e.language === selectedLang)!;

  const tryIt = async () => {
    setLoading(true);
    try {
      const res = await fetch(`/api/docs/try-it`, {
        method: 'POST',
        body: JSON.stringify({ endpoint, method }),
        headers: { 'Content-Type': 'application/json' }
      });
      const data = await res.json();
      setResponse(JSON.stringify(data, null, 2));
    } catch (error) {
      setResponse(`Error: ${error.message}`);
    }
    setLoading(false);
  };

  return (
    <div className="rounded-lg overflow-hidden border">
      {/* Language tabs */}
      <div className="flex border-b bg-gray-100">
        {examples.map((example) => (
          <button
            key={example.language}
            onClick={() => setSelectedLang(example.language)}
            className={`px-4 py-2 text-sm ${
              selectedLang === example.language
                ? 'bg-white border-b-2 border-blue-500'
                : 'text-gray-600'
            }`}
          >
            {example.label}
          </button>
        ))}
      </div>

      {/* Code */}
      <div className="relative">
        <SyntaxHighlighter
          language={selectedLang}
          style={vscDarkPlus}
          customStyle={{ margin: 0, borderRadius: 0 }}
        >
          {currentExample.code}
        </SyntaxHighlighter>

        <button
          onClick={() => navigator.clipboard.writeText(currentExample.code)}
          className="absolute top-2 right-2 p-2 bg-gray-700 rounded hover:bg-gray-600"
          title="Copy code"
        >
          📋
        </button>
      </div>

      {/* Try it button */}
      <div className="p-4 bg-gray-50 border-t">
        <button
          onClick={tryIt}
          disabled={loading}
          className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50"
        >
          {loading ? 'Running...' : 'Try it →'}
        </button>

        {response && (
          <div className="mt-4">
            <h4 className="text-sm font-medium mb-2">Response:</h4>
            <pre className="bg-gray-900 text-gray-100 p-4 rounded text-sm overflow-auto">
              {response}
            </pre>
          </div>
        )}
      </div>
    </div>
  );
}

DX Checklist

Area Metric Target
Documentation Time to first call < 5 minutes
SDKs Languages covered Top 5+
Errors Message clarity 100% actionable
Examples Code snippets Every endpoint
Support Response time < 24 hours
Status Uptime page 99.9%+

Fazit

Exzellente Developer Experience erfordert:

  1. Documentation: Klar, aktuell, interaktiv
  2. SDKs: Type-safe, gut dokumentiert
  3. Errors: Hilfreich, nicht kryptisch
  4. Tools: CLI, Postman, Testing

DX ist Investition in Adoption und Retention.


Bildprompts

  1. "Developer portal dashboard, API keys and usage stats"
  2. "Interactive API documentation, code examples with try-it button"
  3. "CLI tool in terminal, colorful output with progress indicators"

Quellen