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:
- curl-to-json-schema - Generate OpenAPI schemas from curl commands
- massimo-cli - Generate type-safe clients from OpenAPI schemas
- 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
fetchAPI (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:
- curl-to-json-schema transforms curl commands into structured schemas
- massimo-cli generates type-safe, validated API clients from those schemas
- 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 - Generate schemas from curl commands
- massimo - Type-safe HTTP client library
- massimo-cli - Client generator CLI
- OpenAPI Specification - API schema standard
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!






