This directory contains complete examples of using heroku-fetch in oclif-based CLI commands, following the authentication patterns used in the Heroku CLI.
These examples demonstrate:
- ✅ Automatic token management from
HEROKU_API_KEY - ✅ GET, POST, DELETE requests
- ✅ Streaming responses (logs)
- ✅ Multiple services (platform API, data API)
- ✅ Error handling with specific error types
- ✅ Automatic 2FA prompts for sensitive operations (no configuration needed!)
- ✅ JSON and formatted output modes
- ✅ Oclif table formatting and progress indicators
All examples use automatic authentication. No manual token management needed!
import { HerokuApiClient } from '@heroku/heroku-fetch';
// Client automatically gets the token from the HEROKU_API_KEY environment variable
const client = new HerokuApiClient({
service: 'platform',
});To authenticate with credentials saved by the Heroku CLI, resolve a token with
@heroku/heroku-credential-manager
and pass it to the client.
base-command.ts provides reusable base classes that handle authentication, client setup, and error handling:
import { AppCommand } from './base-command.js';
// Extend AppCommand to automatically get:
// - Authentication
// - API client setup
// - Error handling
// - --app flag
export default class MyCommand extends AppCommand {
async run() {
// this.client is already set up
// this.app contains the app name from --app flag
const response = await this.client.get(`/apps/${this.app}`);
// Errors are automatically handled by base class
}
}Simple GET request with table formatting.
Features:
- GET request to
/apps - Table output using
ux.table() - JSON flag support
- Basic error handling
Usage:
heroku apps:list
heroku apps:list --jsonGET request with detailed information display.
Features:
- GET request to
/apps/{app} - Formatted key-value output
- Conditional field display
- 404 error handling
Usage:
heroku apps:info --app my-app
heroku apps:info --app my-app --jsonPOST request with body and validation errors.
Features:
- POST request with JSON body
- Progress indicators with
ux.action - Validation error handling (422)
- Optional arguments and flags
Usage:
heroku apps:create
heroku apps:create my-app-name
heroku apps:create --region eu
heroku apps:create my-app --stack heroku-22Streaming response handling.
Features:
- POST to create log session
- Stream consumption with ReadableStream
- Custom baseURL for logplex
- Real-time output to stdout
- Graceful stream interruption handling
Usage:
heroku logs --app my-app
heroku logs --app my-app --tail
heroku logs --app my-app --num 200
heroku logs --app my-app --dyno web.1Reusable base classes for commands.
Features:
- Automatic authentication setup
- API client initialization
- Consistent error handling
- Shared flags (--json, --app)
- Reduces boilerplate significantly
Classes:
BaseCommand- Base for all commandsAppCommand- Base for app-specific commands (includes --app flag)
Example using AppCommand base class - shows how simple commands become!
Features:
- Extends AppCommand
- Minimal boilerplate
- Automatic error handling
- Clean and focused implementation
Usage:
heroku dynos --app my-app
heroku dynos --app my-app --jsonExample calling the Data API (postgres-api.heroku.com) endpoint.
Features:
- Multiple service clients (platform + data)
- Calls
/client/v11/databases/{id}on data API - Maps config vars to database URLs
- Handles unprovisioned databases
- Sorts databases (DATABASE_URL first)
- Shows database information with formatted output
Usage:
heroku pg:info --app my-app
heroku pg:info --app my-app --json
heroku pg:info DATABASE --app my-appnpm install @heroku/heroku-fetch @oclif/coreCopy any of the example files to your plugin's src/commands directory:
# Example structure
src/
commands/
apps/
list.ts # Copy from apps-list.ts
info.ts # Copy from apps-info.ts
create.ts # Copy from apps-create.ts
logs.ts # Copy from logs.tsThe examples use the pattern:
import { HerokuApiClient } from '@heroku/heroku-fetch';This works out of the box - no additional setup needed!
npm run build
./bin/dev.js apps:list
./bin/dev.js apps:info --app my-appimport { Command } from '@oclif/core';
import { HerokuApiClient, HerokuApiError } from '@heroku/heroku-fetch';
export default class MyCommand extends Command {
async run() {
// Client automatically uses token from the HEROKU_API_KEY env var
const client = new HerokuApiClient({
service: 'platform',
});
try {
const response = await client.get('/endpoint');
const data = await response.json();
this.log(JSON.stringify(data, null, 2));
} catch (error) {
if (error instanceof HerokuApiError) {
this.error(`API Error: ${error.message} (${error.statusCode})`);
}
throw error;
}
}
}// Platform API (default) - token auto-loaded
const platformClient = new HerokuApiClient({
service: 'platform',
});
// Data API (Postgres) - token auto-loaded
const dataClient = new HerokuApiClient({
service: 'data',
});
// Data API with EU region - token auto-loaded
const euDataClient = new HerokuApiClient({
service: 'data',
region: 'eu',
});
// Custom API with static token
const customClient = new HerokuApiClient({
service: 'custom',
baseUrl: 'https://api.custom.com',
token: 'custom-token', // Optional: override auto-loading
});try {
const response = await client.get('/apps/my-app');
const app = await response.json();
} catch (error) {
if (error instanceof NotFoundError) {
this.error('App not found');
} else if (error instanceof AuthenticationError) {
this.error('Authentication failed. Please run: heroku login');
} else if (error instanceof RateLimitError) {
this.error(`Rate limited. Retry after ${error.retryAfter} seconds`);
} else if (error instanceof HerokuApiError) {
// Handle validation errors (422)
if (error.statusCode === 422 && error.errors) {
for (const err of error.errors) {
this.error(`${err.message}`);
}
} else {
this.error(`API Error: ${error.message} (${error.statusCode})`);
}
} else {
throw error;
}
}Two-factor authentication is automatically handled by default! When using @oclif/core, the client will automatically prompt for 2FA codes when needed.
// 2FA is handled automatically - no configuration needed!
const client = new HerokuApiClient({
service: 'platform',
});
// If 2FA is required, user will be prompted automatically
await client.delete('/apps/my-app');To customize the 2FA prompt or use a different prompting mechanism:
import { createCliTwoFactorPrompt } from '@heroku/heroku-fetch';
const client = new HerokuApiClient({
service: 'platform',
twoFactor: {
onChallenge: createCliTwoFactorPrompt({
message: 'Enter your Heroku 2FA code',
}),
},
});Or provide a completely custom handler:
const client = new HerokuApiClient({
service: 'platform',
twoFactor: {
onChallenge: async () => {
// Your custom 2FA logic here
return await getCodeFromSomewhere();
},
},
});- Always check for token: Use
if (!token) this.error(...)to provide clear error messages - Use ux utilities: Leverage
ux.action,ux.table, andux.promptfor better UX - Handle specific errors: Import specific error types for better error handling
- Support --json flag: Add a
--jsonflag for programmatic usage - Use type safety: Define TypeScript interfaces for API responses
Enable debug output for heroku-fetch:
DEBUG=heroku-fetch:* heroku apps:list
DEBUG=heroku-fetch:request,heroku-fetch:response heroku apps:list