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>
644 lines
20 KiB
Markdown
644 lines
20 KiB
Markdown
# 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```tsx
|
|
// 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
|
|
|
|
- [Stripe API Design](https://stripe.com/docs/api)
|
|
- [Twilio Developer Experience](https://www.twilio.com/docs)
|
|
- [GitHub REST API](https://docs.github.com/en/rest)
|
|
- [Developer Experience Principles](https://developerexperience.io/)
|