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
massimoas 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
getHeadersfor dynamic authentication and token refresh - Add generation to your build process to keep types in sync
- Use
throwOnErrorfor cleaner error handling with try/catch
Next Steps
- OAuth Authentication — Set up authentication to get access tokens
- API Reference — Explore all available Parqet Connect endpoints
- Massimo Documentation — Learn more about advanced features
Happy coding! 🚀