Skip to main content

Command Palette

Search for a command to run...

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

Updated
11 min read
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 - Generate OpenAPI schemas from curl commands
  2. massimo-cli - Generate type-safe clients from OpenAPI schemas
  3. massimo - 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

# 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

npm install curl-to-json-schema

Basic Usage

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

curls.txt:

# 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:

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

api-schema.json (generated):

{
  "$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:

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:

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

Or programmatically:

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

npm install --save-dev massimo-cli massimo

Basic Client Generation

# 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:

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

Wraps all parameters in structured objects:

// 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):

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:

massimo api-schema.json -n api --validate-response

Validates all API responses against the schema at runtime.

TypeScript Implementation:

massimo api-schema.json -n api --typescript

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

Types Only:

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

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

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:

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

Then use it in your frontend code:

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:

# 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:

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:

# 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:

#!/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:

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:

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:

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

Advanced: Custom Client Configuration

Authenticated Clients

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

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

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)

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)

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

# 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

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

3. Version Your Schemas

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

4. Separate Dev Dependencies

# 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

{
  "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:

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:

# 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:

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

Try It Now

# 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!

Create Type-Safe API Clients: A Complete Guide