# From curl Commands to Type-Safe API Clients: A Complete Workflow

Have you ever reverse-engineered an API by copying curl commands from your browser's Network tab? Or documented your API with curl examples? What if you could turn those curl commands into production-ready, type-safe HTTP clients automatically?

This post shows you how to combine three powerful tools to create a seamless workflow from curl commands to fully-typed API clients:

1. [**curl-to-json-schema**](https://github.com/platformatic/curl-to-json-schema) - Generate OpenAPI schemas from curl commands
2. [**massimo-cli**](https://massimohttp.dev/getting-started/) - Generate type-safe clients from OpenAPI schemas
3. [**massimo**](https://massimohttp.dev/getting-started/) - Runtime library for making type-safe API calls

## The Problem: APIs Without Types

You're working with an API that either:
- Has no documentation
- Has outdated documentation
- Has documentation but no official SDK
- Is documented only with curl examples

Traditional approach: manually write HTTP client code, hope you got the types right, and discover errors at runtime.

**Better approach:** Automate schema generation and client creation from real curl commands.

## The Solution: A Three-Step Pipeline

```bash
# Step 1: Curl commands → OpenAPI schema
curl-to-json-schema curls.txt -o api-schema.json

# Step 2: OpenAPI schema → Type-safe client
massimo api-schema.json -n myApi --full

# Step 3: Use the generated client
node -e "import('./myApi/myApi.mjs').then(async gen => {
  const client = await gen.default({ url: 'https://api.example.com' })
  const data = await client.getUsers()
  console.log(data)
})"
```

Let's explore each step in detail.

## Step 1: Generate OpenAPI Schema from curl Commands

### Installation

```bash
npm install curl-to-json-schema
```

### Basic Usage

Create a file with your curl commands (one per line):

**curls.txt:**
```bash
# User management endpoints
curl https://api.example.com/users
curl -X POST -H "Content-Type: application/json" -d '{"name":"John","email":"john@example.com"}' https://api.example.com/users
curl -X PUT -d '{"name":"Jane","email":"jane@example.com","age":25}' https://api.example.com/users/123

# Product endpoints
curl https://api.example.com/products
curl -X POST -d '{"name":"Widget","price":29.99,"inStock":true}' https://api.example.com/products
```

Generate the schema:

```bash
curl-to-json-schema curls.txt -o api-schema.json
```

**api-schema.json** (generated):
```json
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "properties": {
    "method": {
      "type": "string",
      "enum": ["GET", "POST", "PUT"]
    },
    "url": {
      "type": "string",
      "format": "uri"
    },
    "headers": {
      "type": "object",
      "properties": {
        "Content-Type": {
          "type": "string",
          "enum": ["application/json"]
        }
      }
    },
    "body": {
      "type": "object",
      "properties": {
        "name": { "type": "string" },
        "email": { "type": "string", "format": "email" },
        "age": { "type": "integer" },
        "price": { "type": "number" },
        "inStock": { "type": "boolean" }
      }
    }
  },
  "required": ["method", "url"]
}
```

### Progressive Schema Building

You can build schemas incrementally as you discover new endpoints:

```javascript
import { CurlToJsonSchema } from 'curl-to-json-schema'

const converter = new CurlToJsonSchema()

// Add commands one at a time
converter.convert('curl https://api.example.com/users')
converter.convert('curl -X POST -d \'{"name":"Alice"}\' https://api.example.com/users')
converter.convert('curl -X DELETE https://api.example.com/users/123')

// Get the accumulated schema
const schema = converter.toSchema()
console.log(JSON.stringify(schema, null, 2))
```

### Extending Existing Schemas

Already have a partial schema? Extend it:

```bash
# Merge new curl commands with existing schema
curl-to-json-schema new-endpoints.txt \
  -s existing-schema.json \
  -o merged-schema.json
```

Or programmatically:

```javascript
import { CurlToJsonSchema } from 'curl-to-json-schema'
import { readFile } from 'node:fs/promises'

const existingSchema = JSON.parse(await readFile('existing-schema.json', 'utf-8'))
const converter = new CurlToJsonSchema({ schema: existingSchema })

// New curl commands will merge with existing schema
converter.convert('curl -X PATCH -d \'{"status":"active"}\' https://api.example.com/users/123')

const mergedSchema = converter.toSchema()
```

### Key Features

- **Smart Type Inference**: Automatically detects strings, numbers, booleans, arrays, objects
- **Format Detection**: Recognizes emails, URLs, UUIDs, dates
- **Schema Merging**: Intelligently combines multiple curl commands into one schema
- **Required Fields**: Marks authentication headers and critical fields as required

## Step 2: Generate Type-Safe Client with massimo-cli

### Installation

```bash
npm install --save-dev massimo-cli massimo
```

### Basic Client Generation

```bash
# Generate from local schema file
massimo ./api-schema.json -n apiClient

# Generate from live OpenAPI URL
massimo http://localhost:3000/documentation/json -n apiClient
```

This creates a folder structure:

```
apiClient/
├── package.json           # Module configuration
├── apiClient.d.ts        # TypeScript type definitions
├── apiClient.mjs         # Client implementation (ESM)
└── apiClient.openapi.json # Copy of the schema
```

### Client Generation Options

**Full Request/Response Mode:**
```bash
massimo api-schema.json -n api --full
```

Wraps all parameters in structured objects:

```javascript
// Without --full
await client.createUser({ name: 'John', email: 'john@example.com' })

// With --full
await client.createUser({
  body: { name: 'John', email: 'john@example.com' },
  headers: { 'Authorization': 'Bearer token' },
  query: { validate: true }
})
```

**Frontend Client (Browser):**
```bash
massimo api-schema.json \
  --frontend \
  --language ts \
  --name browserApi \
  --with-credentials
```

Generates a browser-compatible client using the native `fetch` API instead of Node.js HTTP. This is perfect for client-side applications, SPAs, and any browser-based code. The frontend client is lightweight, has no Node.js dependencies, and works in all modern browsers.

**Response Validation:**
```bash
massimo api-schema.json -n api --validate-response
```

Validates all API responses against the schema at runtime.

**TypeScript Implementation:**
```bash
massimo api-schema.json -n api --typescript
```

Generates `.ts` files instead of `.mjs`, giving you full TypeScript source code.

**Types Only:**
```bash
massimo api-schema.json -n apiTypes --types-only
```

Just the type definitions, no implementation - useful if you want to write your own client.

### Using the Generated Client

```javascript
import generateApiClient from './apiClient/apiClient.mjs'

// Initialize the client
const client = await generateApiClient({
  url: 'https://api.example.com',
  headers: { 'Authorization': 'Bearer your-token' },
  throwOnError: true,           // Throw on HTTP errors
  validateResponse: true,        // Validate responses
  bodyTimeout: 30000,           // Request timeout

  // Dynamic headers per request
  getHeaders: async (options) => {
    return { 'X-Request-ID': crypto.randomUUID() }
  }
})

// Make API calls (fully typed!)
const users = await client.getUsers({ query: { page: 1, limit: 10 } })
const newUser = await client.createUser({
  body: { name: 'John', email: 'john@example.com' }
})
```

### TypeScript Support

```typescript
import generateApiClient from './apiClient/apiClient.mjs'
import type { ApiClient, User } from './apiClient/apiClient.d.ts'

const client: ApiClient = await generateApiClient({
  url: 'https://api.example.com'
})

// All operations are fully typed
const users: User[] = await client.getUsers()
const newUser: User = await client.createUser({
  body: { name: 'Jane', email: 'jane@example.com' }
})
```

## Frontend Client Usage

For browser-based applications, use the `--frontend` flag to generate a fetch-based client:

```bash
massimo api-schema.json \
  --frontend \
  --language ts \
  --name apiClient \
  --with-credentials
```

Then use it in your frontend code:

```typescript
import generateApiClient from './apiClient/apiClient.js'

// Works in the browser with native fetch
const client = await generateApiClient({
  url: 'https://api.example.com',

  // Optional: include credentials (cookies)
  getHeaders: async () => {
    return {
      'Authorization': `Bearer ${localStorage.getItem('token')}`,
      'X-Client-Version': '1.0.0'
    }
  }
})

// Make API calls from the browser
const users = await client.getUsers({ query: { page: 1 } })
const newUser = await client.createUser({
  body: { name: 'Jane', email: 'jane@example.com' }
})
```

The frontend client:
- Uses native `fetch` API (no Node.js dependencies)
- Supports CORS and credentials
- Works with all modern browsers
- Can be bundled with Vite, Webpack, or other bundlers
- Fully type-safe when using TypeScript

## Real-World Workflows

### Workflow 1: Reverse Engineering APIs

You're integrating with a third-party API that has no SDK:

```bash
# 1. Inspect network traffic and save curl commands
# (Copy from browser DevTools → Network → Right click → Copy as cURL)

# 2. Save curl commands to a file
cat > api-calls.txt << 'EOF'
curl 'https://external-api.com/users' -H 'Authorization: Bearer demo'
curl -X POST 'https://external-api.com/users' -H 'Content-Type: application/json' -d '{"name":"Test"}'
EOF

# 3. Generate OpenAPI schema
curl-to-json-schema api-calls.txt -o external-api-schema.json

# 4. Generate type-safe client
massimo external-api-schema.json -n externalApi --full --validate-response

# 5. Use in your application
```

**app.js:**
```javascript
import generateExternalApiClient from './externalApi/externalApi.mjs'

const client = await generateExternalApiClient({
  url: 'https://external-api.com',
  headers: { 'Authorization': `Bearer ${process.env.API_TOKEN}` }
})

// Now you have type-safe API calls!
const users = await client.getUsers()
```

### Workflow 2: Documentation-Driven Development

Your team documents APIs with curl examples:

**docs/api-examples.txt:**
```bash
# Authentication
curl -X POST https://api.myapp.com/auth/login -d '{"username":"user","password":"pass"}'

# User operations
curl https://api.myapp.com/users -H "Authorization: Bearer TOKEN"
curl -X POST https://api.myapp.com/users -d '{"name":"John","email":"john@example.com"}'
```

**CI/CD Pipeline:**
```bash
#!/bin/bash
# .github/workflows/generate-clients.yml

# Generate schema from curl examples
curl-to-json-schema docs/api-examples.txt -o schema/openapi.json

# Generate clients for different consumers
massimo schema/openapi.json -n nodeClient --full
massimo schema/openapi.json -n browserClient --frontend --language ts

# Publish clients to npm
npm publish ./nodeClient
npm publish ./browserClient
```

Now consumers install your SDK package instead of making raw HTTP calls.

### Workflow 3: API Testing

Use the generated schema for testing:

```javascript
import { CurlToJsonSchema } from 'curl-to-json-schema'
import { buildOpenAPIClient } from 'massimo'
import { test } from 'node:test'
import assert from 'node:assert'

test('API matches documented curl examples', async () => {
  // Generate expected schema from documentation
  const converter = new CurlToJsonSchema()
  converter.convert('curl https://api.example.com/users')
  const expectedSchema = converter.toSchema()

  // Build client and test real API
  const client = await buildOpenAPIClient({
    url: 'https://api.example.com',
    validateResponse: true  // This will throw if response doesn't match schema
  })

  // If this doesn't throw, API matches documentation
  const users = await client.getUsers()
  assert(Array.isArray(users))
})
```

### Workflow 4: Incremental Schema Discovery

Building an API wrapper and discovering endpoints as you go:

```javascript
import { CurlToJsonSchema } from 'curl-to-json-schema'
import { readFile, writeFile } from 'node:fs/promises'
import { existsSync } from 'node:fs'

class ApiSchemaBuilder {
  constructor(schemaPath) {
    this.schemaPath = schemaPath
    this.converter = new CurlToJsonSchema()
  }

  async init() {
    // Load existing schema if available
    if (existsSync(this.schemaPath)) {
      const existing = JSON.parse(await readFile(this.schemaPath, 'utf-8'))
      this.converter = new CurlToJsonSchema({ schema: existing })
    }
  }

  async addCurl(curlCommand) {
    this.converter.convert(curlCommand)
    await this.save()
  }

  async save() {
    const schema = this.converter.toSchema()
    await writeFile(this.schemaPath, JSON.stringify(schema, null, 2))
  }
}

// Usage
const builder = new ApiSchemaBuilder('./api-schema.json')
await builder.init()

// Discover endpoints as you work
await builder.addCurl('curl https://api.example.com/users')
await builder.addCurl('curl https://api.example.com/products')
await builder.addCurl('curl -X POST -d \'{"name":"New"}\' https://api.example.com/products')

// Schema file is continuously updated
```

Then periodically regenerate your client:

```bash
massimo api-schema.json -n api --full
```

## Advanced: Custom Client Configuration

### Authenticated Clients

```javascript
import generateApiClient from './api/api.mjs'

const client = await generateApiClient({
  url: 'https://api.example.com',

  // Static headers
  headers: {
    'Authorization': `Bearer ${process.env.API_TOKEN}`,
    'X-API-Version': '2024-01-01'
  },

  // Dynamic headers per request
  getHeaders: async (options) => {
    const token = await refreshTokenIfNeeded()
    return {
      'Authorization': `Bearer ${token}`,
      'X-Request-ID': crypto.randomUUID(),
      'X-Timestamp': new Date().toISOString()
    }
  }
})
```

### Error Handling

```javascript
const client = await generateApiClient({
  url: 'https://api.example.com',
  throwOnError: false  // Don't throw, handle manually
})

const response = await client.getUsers()

if (response.statusCode >= 400) {
  console.error('API Error:', response.statusCode, response.body)
} else {
  console.log('Success:', response.body)
}
```

### Timeout Configuration

```javascript
const client = await generateApiClient({
  url: 'https://api.example.com',
  bodyTimeout: 30000,      // 30 second body timeout
  headersTimeout: 10000,   // 10 second header timeout
})
```

## Comparison: Before and After

### Before (Manual HTTP Calls)

```javascript
import fetch from 'node-fetch'

async function getUsers(page = 1) {
  const response = await fetch(
    `https://api.example.com/users?page=${page}`,
    {
      headers: {
        'Authorization': 'Bearer token',
        'Content-Type': 'application/json'
      }
    }
  )

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`)
  }

  return response.json()  // No type safety
}

async function createUser(data) {
  const response = await fetch(
    'https://api.example.com/users',
    {
      method: 'POST',
      headers: {
        'Authorization': 'Bearer token',
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(data)  // No validation
    }
  )

  return response.json()
}

// Usage - no autocomplete, no type checking
const users = await getUsers(1)
const newUser = await createUser({ name: 'John' })  // Typos possible
```

### After (Generated Type-Safe Client)

```javascript
import generateApiClient from './api/api.mjs'

const client = await generateApiClient({
  url: 'https://api.example.com',
  headers: { 'Authorization': 'Bearer token' },
  validateResponse: true,
  throwOnError: true
})

// Usage - fully typed, autocomplete, validation
const users = await client.getUsers({ query: { page: 1 } })
const newUser = await client.createUser({
  body: { name: 'John', email: 'john@example.com' }
})  // TypeScript will catch missing/wrong fields
```

## Best Practices

### 1. Keep curl Examples Updated

```bash
# Store curl examples in version control
docs/
  api-examples/
    users.txt
    products.txt
    auth.txt

# Regenerate schema in CI
curl-to-json-schema docs/api-examples/*.txt -o schema/openapi.json
```

### 2. Validate Real API Calls

```javascript
const client = await generateApiClient({
  url: process.env.API_URL,
  validateResponse: true,  // Catch schema drift
  throwOnError: true
})
```

### 3. Version Your Schemas

```bash
# Tag schema versions
cp api-schema.json schemas/v1.0.0.json
git tag api-schema-v1.0.0
```

### 4. Separate Dev Dependencies

```bash
# massimo-cli is only needed for code generation
npm install --save-dev massimo-cli curl-to-json-schema

# massimo runtime is needed in production
npm install massimo
```

### 5. Automate Client Regeneration

```json
{
  "scripts": {
    "generate:schema": "curl-to-json-schema docs/api-examples.txt -o api-schema.json",
    "generate:client": "massimo api-schema.json -n api --full --validate-response",
    "generate": "npm run generate:schema && npm run generate:client"
  }
}
```

## Troubleshooting

### Issue: Generated Client Has No Methods

**Problem:** Schema doesn't define operations/paths.

**Solution:** curl-to-json-schema generates a JSON Schema, but massimo expects OpenAPI format. Convert manually:

```javascript
import { readFile, writeFile } from 'node:fs/promises'

const jsonSchema = JSON.parse(await readFile('api-schema.json', 'utf-8'))

const openApiSchema = {
  openapi: '3.0.0',
  info: { title: 'API', version: '1.0.0' },
  paths: {
    '/users': {
      get: {
        operationId: 'getUsers',
        responses: {
          '200': {
            description: 'Success',
            content: {
              'application/json': { schema: jsonSchema }
            }
          }
        }
      }
    }
  }
}

await writeFile('openapi.json', JSON.stringify(openApiSchema, null, 2))
```

### Issue: Type Errors in Generated Client

**Problem:** Schema has conflicting types.

**Solution:** Review your curl commands - you might be sending different types to the same endpoint:

```bash
# These create conflicting schemas:
curl -d '{"age":25}' https://api.example.com/users      # age: integer
curl -d '{"age":"25"}' https://api.example.com/users    # age: string

# Fix: Be consistent
curl -d '{"age":25}' https://api.example.com/users
```

### Issue: Authentication Fails

**Problem:** Headers not being sent correctly.

**Solution:** Use `getHeaders` for dynamic auth:

```javascript
const client = await generateApiClient({
  url: 'https://api.example.com',
  getHeaders: async () => {
    const token = await getAuthToken()
    return { 'Authorization': `Bearer ${token}` }
  }
})
```

## Conclusion

The combination of **curl-to-json-schema**, **massimo-cli**, and **massimo** creates a powerful workflow:

1. **curl-to-json-schema** transforms curl commands into structured schemas
2. **massimo-cli** generates type-safe, validated API clients from those schemas
3. **massimo** provides the runtime to make those API calls with confidence

This pipeline is perfect for:
- Reverse engineering APIs without documentation
- Creating SDKs from API examples
- Ensuring API consumers stay in sync with your API
- Building type-safe microservice communication
- Testing APIs against their documentation

No more manual HTTP client code. No more runtime type errors. Just capture curl commands and get production-ready, type-safe API clients.

## Resources

- [curl-to-json-schema](https://github.com/matteo) - Generate schemas from curl commands
- [massimo](https://github.com/platformatic/massimo) - Type-safe HTTP client library
- [massimo-cli](https://github.com/platformatic/massimo) - Client generator CLI
- [OpenAPI Specification](https://swagger.io/specification/) - API schema standard

## Try It Now

```bash
# Install all tools
npm install curl-to-json-schema massimo
npm install --save-dev massimo-cli

# Create example curl commands
cat > api.txt << 'EOF'
curl https://jsonplaceholder.typicode.com/users
curl https://jsonplaceholder.typicode.com/posts
EOF

# Generate schema
curl-to-json-schema api.txt -o schema.json

# Generate client (note: you'll need to convert to OpenAPI format first)
massimo schema.json -n myClient

# Start building!
```

Happy API client building!
