Connect

Generating Typesafe API Clients for Parqet Connect

Learn how to generate fully type-safe API clients from the Parqet Connect OpenAPI specification using Massimo, the modern CLI tool from Platformatic. Say goodbye to manual type definitions and runtime errors.
Tobi

Tobi

Author

Generating Typesafe API Clients for Parqet Connect

Generating Typesafe API Clients for Parqet Connect

Building integrations with external APIs often means writing repetitive boilerplate code and manually maintaining TypeScript types. But what if you could generate a fully type-safe API client directly from an OpenAPI specification? In this tutorial, we'll explore how to use Massimo from Platformatic to generate type-safe clients for the Parqet Connect API.

Why Type-Safe API Clients?

When building integrations with the Parqet Connect API, a common starting point is writing manual HTTP calls:

const response = await fetch("https://connect.parqet.com/portfolios", {
  headers: { Authorization: `Bearer ${accessToken}` },
});
const portfolios = await response.json(); // Type: any

While this works, it doesn't scale well as your integration grows. A generated type-safe client offers significant advantages:

  • Better developer experience — Full autocomplete for endpoints, parameters, and response properties
  • Early error detection — Catch mistakes during development, not in production
  • Safer refactoring — Type information ensures changes propagate correctly across your codebase
  • Improved LLM assistance — Type definitions provide rich context for AI coding assistants

A generated type-safe client delivers these benefits automatically:

const { body: portfolios } = await client.getPortfolios({}); // Type: Portfolio[]

With full TypeScript integration, your IDE provides autocomplete, type checking catches mismatches during development, and regenerating the client keeps your code aligned with the latest API specification.

What is Massimo?

Massimo is a modern API SDK generator from the Platformatic team. It creates fully-typed TypeScript/JavaScript clients from OpenAPI specifications with:

  • OpenAPI 3.x support — Full support for modern OpenAPI specifications
  • TypeScript first — Automatic type generation for requests and responses
  • Multiple targets — Generate clients for Node.js (using Undici) or browsers (using Fetch)
  • Framework agnostic — Works with any Node.js application or frontend framework
  • Zero runtime dependencies — The generated frontend client uses native Fetch API

The name "Massimo" is inspired by Massimo Troisi, the beloved Italian actor from "Il Postino" (The Postman). Just as the postman delivered messages and connected people, Massimo delivers API connections and bridges the gap between services.

Prerequisites

Before we begin, ensure you have:

  • Node.js 24+ installed
  • A Parqet Connect integration with valid access tokens (see our OAuth tutorial)

Installing Massimo

Install the Massimo CLI globally:

pnpm add -g massimo-cli

Or use it directly with pnpm dlx:

pnpm dlx massimo-cli

Generating Your First Client

The Parqet Connect API exposes its OpenAPI specification at a public endpoint. Let's generate a client from it:

massimo https://developer.parqet.com/api-spec/current.json \
  --name parqet \
  --skip-prefixed-url

This command:

  • Fetches the OpenAPI spec from the Parqet Developer Hub
  • Generates a TypeScript Node.js client (using Undici for optimal performance)
  • Outputs files to a parqet/ directory

After running this, you'll have:

parqet/
├── parqet.mjs           # Implementation with typed client factory
├── parqet.d.ts          # TypeScript type definitions
└── parqet.openapi.json  # Local copy of the OpenAPI spec

Understanding the Generated Code

Let's explore what Massimo generates:

Type Definitions

The parqet.d.ts file contains all the TypeScript types derived from the OpenAPI spec:

import { type PlatformaticClientOptions } from "massimo";

// Request types
export type GetPortfoliosRequest = {
  // Query parameters, headers, etc.
};

// Response types
export type GetPortfoliosResponseOK = Array<{
  id: string;
  name: string;
  currency: string;
  // ... other portfolio properties
}>;

// Union type for all possible responses
export type GetPortfoliosResponses = GetPortfoliosResponseOK;

// Client interface
export type Parqet = {
  getPortfolios(req: GetPortfoliosRequest): Promise<GetPortfoliosResponses>;
  getPortfolio(req: GetPortfolioRequest): Promise<GetPortfolioResponses>;
  // ... other operations
};

// Factory function
export function generateParqetClient(opts: PlatformaticClientOptions): Promise<Parqet>;
export default generateParqetClient;

Implementation

The parqet.mjs file contains the client factory that creates a configured API client:

import { buildOpenAPIClient } from "massimo";
import { join } from "node:path";

export async function generateParqetClient(opts) {
  return buildOpenAPIClient({
    type: "openapi",
    name: "parqet",
    path: join(import.meta.dirname, "parqet.openapi.json"),
    url: opts.url,
    throwOnError: opts.throwOnError,
    fullResponse: true,
    fullRequest: true,
    getHeaders: opts.getHeaders,
  });
}

export default generateParqetClient;

The generated client uses Undici, Node.js's built-in HTTP client, providing:

  • Connection pooling — Reuses connections for better performance
  • HTTP/2 support — Automatic protocol negotiation
  • Keep-alive — Persistent connections reduce latency

Using the Generated Client

Basic Usage

Here's how to use the generated client in your Node.js application:

import { generateParqetClient } from "./parqet/parqet.mjs";

// Create a configured client instance
const client = await generateParqetClient({
  url: "https://connect.parqet.com",
});

// Make type-safe API calls
const portfolios = await client.getPortfolios({
  headers: {
    Authorization: `Bearer ${accessToken}`,
  },
});
// TypeScript knows: portfolios.body is Portfolio[]

for (const portfolio of portfolios.body) {
  console.log(`Portfolio: ${portfolio.name}`); // Full autocomplete!

  const details = await client.getPortfolio({
    id: portfolio.id,
    headers: { Authorization: `Bearer ${accessToken}` },
  });
  console.log(`  Holdings: ${details.body.holdings.length}`);
}

Using Dynamic Headers

For cleaner code, configure headers dynamically with getHeaders:

import { generateParqetClient } from "./parqet/parqet.mjs";

const client = await generateParqetClient({
  url: "https://connect.parqet.com",
  async getHeaders() {
    return {
      Authorization: `Bearer ${await getAccessToken()}`,
    };
  },
});

// Headers are automatically added to every request
const portfolios = await client.getPortfolios({});
const user = await client.getUser({});

Handling Token Refresh

The getHeaders function is called before every request, making it perfect for token refresh logic:

import { generateParqetClient } from "./parqet/parqet.mjs";

let cachedToken: string | null = null;
let tokenExpiry: Date | null = null;

async function getValidAccessToken(): Promise<string> {
  if (cachedToken && tokenExpiry && tokenExpiry > new Date()) {
    return cachedToken;
  }

  // Refresh the token
  const { accessToken, expiresAt } = await refreshAccessToken();
  cachedToken = accessToken;
  tokenExpiry = expiresAt;

  return cachedToken;
}

const client = await generateParqetClient({
  url: "https://connect.parqet.com",
  async getHeaders() {
    const token = await getValidAccessToken();
    return {
      Authorization: `Bearer ${token}`,
    };
  },
});

// Tokens are automatically refreshed when needed
const portfolios = await client.getPortfolios({});

Advanced Options

Massimo provides several options to customize the generated client:

Types Only

If you only need TypeScript types (perhaps you prefer writing your own HTTP logic):

massimo https://developer.parqet.com/api-spec/current.json \
  --name parqet \
  --types-only \
  --skip-prefixed-url

This generates only the .d.ts file with all type definitions.

Simplified Response Format

By default, the client returns full response objects with status codes and headers. For simpler usage, you can get just the response body:

massimo https://developer.parqet.com/api-spec/current.json \
  --name parqet \
  --full false \
  --skip-prefixed-url

This changes the return type to return just the body:

// With --full false
const portfolios = await client.getPortfolios({});
// portfolios is Portfolio[] directly

// Default (full response)
const result = await client.getPortfolios({});
// result.statusCode: number
// result.headers: Record<string, string>
// result.body: Portfolio[]

Module Format

Explicitly specify the module format:

# ESM (default for modern projects)
massimo ... --module esm

# CommonJS (for older Node.js projects)
massimo ... --module cjs

Error Handling with throwOnError

Configure the client to throw on HTTP errors:

const client = await generateParqetClient({
  url: "https://connect.parqet.com",
  throwOnError: true, // Throws on 4xx/5xx responses
});

Integrating with Your Build Process

Add client generation to your package.json scripts:

{
  "scripts": {
    "generate:api-client": "massimo https://developer.parqet.com/api-spec/current.json --name parqet --skip-prefixed-url",
    "postinstall": "pnpm run generate:api-client"
  }
}

This ensures the client is regenerated whenever you install dependencies, keeping types in sync with the API.

Note: You'll need to install massimo as a dependency in your project:

pnpm add massimo

Error Handling Best Practices

When using throwOnError: true, the client throws errors for non-2xx responses. Here's how to handle them gracefully:

import { generateParqetClient } from "./parqet/parqet.mjs";

const client = await generateParqetClient({
  url: "https://connect.parqet.com",
  throwOnError: true,
  async getHeaders() {
    return { Authorization: `Bearer ${accessToken}` };
  },
});

async function fetchPortfoliosSafely() {
  try {
    const result = await client.getPortfolios({});
    return result.body;
  } catch (error) {
    if (error.statusCode === 401) {
      console.error("Unauthorized - token may have expired");
    } else if (error.statusCode === 429) {
      console.error("Rate limited - slow down requests");
    } else {
      console.error("API Error:", error.message);
    }
    return [];
  }
}

Without throwOnError, you can check the status code manually:

const result = await client.getPortfolios({});

if (result.statusCode !== 200) {
  console.error(`Request failed with status ${result.statusCode}`);
  return;
}

const portfolios = result.body;

Real-World Example: Portfolio Creation Service

Let's put it all together with a practical example — a service that creates a new portfolio in Parqet:

import { generateParqetClient, type Parqet } from "./parqet/parqet.mjs";

async function createParqetService(accessToken: string): Promise<Parqet> {
  return generateParqetClient({
    url: "https://connect.parqet.com",
    async getHeaders() {
      return { Authorization: `Bearer ${accessToken}` };
    },
  });
}

async function createPortfolioInParqet(accessToken: string, name: string) {
  const client = await createParqetService(accessToken);

  // Fetch user info to verify the connection
  const { body: userInfo } = await client.userInfo({});
  console.log(`Creating portfolio for user: ${userInfo.userId}`);

  // Create a new portfolio
  const { body: portfolio } = await client.portfoliosCreate({
    name,
  });

  console.log(`Created portfolio with ID: ${portfolio.id}`);

  // Fetch the portfolio details to confirm
  const { body: details } = await client.portfoliosRetrieve({
    portfolioId: portfolio.id,
  });

  console.log(`Portfolio "${details.name}" created at ${details.createdAt}`);
  console.log(`Currency: ${details.currency}`);

  return portfolio;
}

// Usage
const portfolio = await createPortfolioInParqet(accessToken, "My Broker Import");

Notice how TypeScript provides full autocomplete and type checking throughout — from user info to portfolio creation and retrieval. Any API changes will immediately surface as TypeScript errors, not runtime bugs.

Conclusion

Generating type-safe API clients with Massimo eliminates an entire class of bugs and makes your codebase more maintainable. By deriving types directly from the OpenAPI specification, you ensure your client always matches the API contract.

Key takeaways:

  • Use the default Node.js client for backend integrations with Parqet Connect
  • Leverage getHeaders for dynamic authentication and token refresh
  • Add generation to your build process to keep types in sync
  • Use throwOnError for cleaner error handling with try/catch

Next Steps

Happy coding! 🚀