Getting Started
Getting Started with Massimo
Section titled “Getting Started with Massimo”Massimo is a powerful tool for generating typed HTTP clients from OpenAPI and GraphQL APIs. This guide will help you get started quickly.
Installation
Section titled “Installation”You can use Massimo directly via npx without installation:
npx massimo-cli [options]Or install it globally:
npm install -g massimo-cli# Then use:massimo [options]Creating Your First Client
Section titled “Creating Your First Client”OpenAPI Client
Section titled “OpenAPI Client”To create a client for an OpenAPI API:
npx massimo-cli http://example.com/openapi.json --name myclientThis command will:
- Download the OpenAPI specification
- Generate a typed client in the
./myclientdirectory - Create TypeScript type definitions
- Automatically detect module format (ESM or CommonJS) from your
package.json
GraphQL Client
Section titled “GraphQL Client”For GraphQL APIs:
npx massimo-cli http://example.com/graphql --name myclient --type graphqlForcing Client Type
Section titled “Forcing Client Type”If an API supports both OpenAPI and GraphQL, you can specify which client to generate:
npx massimo-cli http://example.com/api --name myclient --type openapiModule Format (ESM vs CommonJS)
Section titled “Module Format (ESM vs CommonJS)”Massimo automatically detects your project’s module format from package.json. You can also explicitly specify the format:
# Generate ESM module (default for projects with "type": "module")npx massimo-cli http://example.com/openapi.json --name myclient --module esm
# Generate CommonJS module (default for projects without "type": "module")npx massimo-cli http://example.com/openapi.json --name myclient --module cjsExample Usage in JavaScript (GraphQL)
Section titled “Example Usage in JavaScript (GraphQL)”ESM (ECMAScript Module)
Section titled “ESM (ECMAScript Module)”Use the client in your JavaScript application with ESM syntax:
import myClient from "./myclient/myclient.mjs";
/** @type {import('fastify').FastifyPluginAsync<{}> */export default async function (app, opts) { const client = await myClient({ url: "URL" });
app.post("/", async (request, reply) => { const res = await client.graphql({ query: "query { movies { title } }", }); return res; });}CommonJS
Section titled “CommonJS”Use the client with CommonJS syntax:
const myClient = require("./myclient/myclient.cjs");
/** @type {import('fastify').FastifyPluginAsync<{}> */module.exports = async function (app, opts) { const client = await myClient({ url: "URL" });
app.post("/", async (request, reply) => { const res = await client.graphql({ query: "query { movies { title } }", }); return res; });}Example Usage in TypeScript (OpenAPI)
Section titled “Example Usage in TypeScript (OpenAPI)”ESM (ECMAScript Module)
Section titled “ESM (ECMAScript Module)”Use the client in TypeScript application with ESM syntax:
import { type FastifyInstance } from "fastify";import myClient from "./myclient/myclient.mjs";
export default async function (app: FastifyInstance) { const client = await myClient({ url: "URL" });
app.get("/", async (request, reply) => { return client.get({}); });}CommonJS with TypeScript
Section titled “CommonJS with TypeScript”For CommonJS projects using TypeScript:
import { type FastifyInstance } from "fastify";const myClient = require("./myclient/myclient.cjs");
export = async function (app: FastifyInstance) { const client = await myClient({ url: "URL" });
app.get("/", async (request, reply) => { return client.get({}); });}Note that the generator will also update the .env and .env.sample files if they exist.
Generating a client for an unexposed application running within Platformatic Runtime
Section titled “Generating a client for an unexposed application running within Platformatic Runtime”You can use the Platformatic Management API to extract the schema of an application which is not exposed.
Let’s say you have an application created via wattpm create:
$ npx wattpm createHello User, welcome to Watt 3.0.0!? Where would you like to create your project? example? Which package manager do you want to use? pnpm? Which kind of application do you want to create? @platformatic/db? What is the name of the application? movies? What is the connection string? sqlite://./db.sqlite? Do you want to create default migrations? yes? Do you want to use TypeScript? no? Do you want to create another application? yes? Which kind of application do you want to create? @platformatic/service? What is the name of the application? main? Do you want to use TypeScript? no? Do you want to create another application? no? Which application should be exposed? main? What port do you want to use? 3042As you can see, the movies application is not exposed so it is not possible to pass the URL to massimo.
To download the OpenAPI schema, you can use the wattpm inject command:
$ npx wattpm inject movies -p /documentation/json > openapi.jsonNow you can generate a client
$ npx --package massimo-cli plt-client --name movies -f web/main/movies openapi.jsonThis will create the client in the web/main/movies folder.
Now you can modify your web/main/routes/root.js file to add another route to use the new client:
import myClient from "./movies/movies.mjs";
export default async function (fastify, opts) { fastify.get("/example", async (request, reply) => { return { hello: fastify.example }; });
fastify.get("/movies", async (request, reply) => { const movies = await myClient.getMovies(); return movies; });}Finally, test your application by doing:
curl http://127.0.0.1:3042/moviesTypes Generator
Section titled “Types Generator”Types for the client are automatically generated for both OpenAPI and GraphQL schemas. You can generate only the types with the --types-only flag.
Example
Section titled “Example”$ npx --package massimo-cli plt-client http://example.com/to/schema/file --name myclient --types-onlyThis will create the single myclient.d.ts file.
TypeScript Declaration File Extensions
Section titled “TypeScript Declaration File Extensions”With the --type-extension flag, Massimo generates module-specific TypeScript declaration files:
# Generate .d.mts for ESM modules$ npx --package massimo-cli plt-client http://example.com/to/schema/file --name myclient --types-only --type-extension --module esm
# Generate .d.cts for CommonJS modules$ npx --package massimo-cli plt-client http://example.com/to/schema/file --name myclient --types-only --type-extension --module cjs
# Auto-detect module format and generate appropriate extension$ npx --package massimo-cli plt-client http://example.com/to/schema/file --name myclient --types-only --type-extensionWhen --type-extension is used:
- ESM modules generate
.d.mtsfiles - CommonJS modules generate
.d.ctsfiles - Module format is auto-detected from package.json or can be explicitly set with
--module
OpenAPI Types
Section titled “OpenAPI Types”We provide a fully typed experience for OpenAPI, typing both the request and response for each individual OpenAPI operation. Take a look at the example below:
// Omitting some types for brevity
interface Client { getMovies(req: GetMoviesRequest): Promise<Array<GetMoviesResponse>>; createMovie(req: CreateMovieRequest): Promise<CreateMovieResponse>; updateMovies(req: UpdateMoviesRequest): Promise<Array<UpdateMoviesResponse>>; getMovieById(req: GetMovieByIdRequest): Promise<GetMovieByIdResponse>; updateMovie(req: UpdateMovieRequest): Promise<UpdateMovieResponse>; updateMovie(req: UpdateMovieRequest): Promise<UpdateMovieResponse>; deleteMovies(req: DeleteMoviesRequest): Promise<DeleteMoviesResponse>; getQuotesForMovie( req: GetQuotesForMovieRequest, ): Promise<Array<GetQuotesForMovieResponse>>; getQuotes(req: GetQuotesRequest): Promise<Array<GetQuotesResponse>>; createQuote(req: CreateQuoteRequest): Promise<CreateQuoteResponse>; updateQuotes(req: UpdateQuotesRequest): Promise<Array<UpdateQuotesResponse>>; getQuoteById(req: GetQuoteByIdRequest): Promise<GetQuoteByIdResponse>; updateQuote(req: UpdateQuoteRequest): Promise<UpdateQuoteResponse>; updateQuote(req: UpdateQuoteRequest): Promise<UpdateQuoteResponse>; deleteQuotes(req: DeleteQuotesRequest): Promise<DeleteQuotesResponse>; getMovieForQuote( req: GetMovieForQuoteRequest, ): Promise<GetMovieForQuoteResponse>;}
export function generateQuotesClient( opts: PlatformaticClientOptions,): Promise<Client>;export default generateQuotesClient;GraphQL Types
Section titled “GraphQL Types”We provide a partially typed experience for GraphQL, because we do not want to limit how you are going to query the remote system. Take a look at this example:
export interface Movie { id?: string;
title?: string;
releasedDate?: string;
createdAt?: string;
preferred?: string;
quotes?: Array<Quote>;}export interface Quote { id?: string;
quote?: string;
likes?: number;
dislikes?: number;
movie?: Movie;}export interface MoviesCount { total?: number;}export interface QuotesCount { total?: number;}export interface MovieDeleted { id?: string;}export interface QuoteDeleted { id?: string;}
interface GraphQLQueryOptions { query: string; headers: Record<string, string>; variables: Record<string, unknown>;}
interface GraphQLClient { graphql<T>(options: GraphQLQueryOptions): Promise<T>;}
export function generateQuotesClient( opts: PlatformaticClientOptions,): Promise<GraphQLClient>;export default generateQuotesClient;Given only you can know what GraphQL query you are producing, you are responsible for typing it accordingly.
Use the Fastify plugin
Section titled “Use the Fastify plugin”ESM (ECMAScript Module)
Section titled “ESM (ECMAScript Module)”import fastify from "fastify";import pltClient from "massimo/fastify-plugin";
const server = fastify();server.register(pltClient, { url: "http://example.com", type: "graphql" });
// GraphQLserver.post("/", async (request, reply) => { const res = await request.movies.graphql({ query: 'mutation { saveMovie(input: { title: "foo" }) { id, title } }', }); return res;});
// OpenAPI (you need to register the plugin with `openapi` type)server.post("/", async (request, reply) => { const res = await request.movies.createMovie({ title: "foo" }); return res;});
server.listen({ port: 3000 });CommonJS
Section titled “CommonJS”const fastify = require("fastify")();const pltClient = require("massimo/fastify-plugin");
fastify.register(pltClient, { url: "http://example.com", type: "graphql" });
// GraphQLfastify.post("/", async (request, reply) => { const res = await request.movies.graphql({ query: 'mutation { saveMovie(input: { title: "foo" }) { id, title } }', }); return res;});
// OpenAPI (you need to register the plugin with `openapi` type)fastify.post("/", async (request, reply) => { const res = await request.movies.createMovie({ title: "foo" }); return res;});
fastify.listen({ port: 3000 });Note that you would need to install massimo as a dependency.
Adding types information to the fastify Plugin
Section titled “Adding types information to the fastify Plugin”To add types information to your plugin, you can either extend the FastifyRequest interface globally or locally.
import { type MoviesClient } from "./movies/movies";import fastify, { type FastifyRequest, type FastifyReply } from "fastify";import pltClient from "massimo/fastify-plugin";
const server = fastify();server.register(pltClient, { url: "http://example.com", type: "openapi" });
// Method A: extend the interface globallydeclare module "fastify" { interface FastifyRequest { movies: MoviesClient; }}
server.get("/movies", async (request: FastifyRequest, reply: FastifyReply) => { return request.movies.getMovies();});
// Method B: use a local request extensioninterface MoviesRequest extends FastifyRequest { movies: MoviesClient;}
server.get("/movies", async (request: MoviesRequest, reply: FastifyReply) => { return request.movies.getMovies();});Method Names in OpenAPI
Section titled “Method Names in OpenAPI”The names of the operations are defined in the OpenAPI specification using the operationId. If it’s not specified, the name is generated by combining the parts of the path, like /something/{param1}/ and a method GET, it generates getSomethingParam1.
Authentication
Section titled “Authentication”To add necessary headers for downstream services requiring authentication, configure them in your plugin:
/// <reference path="./myclient" />
/** @type {import('fastify').FastifyPluginAsync<{}> */module.exports = async function (app, opts) { app.configureMyclient({ async getHeaders(req, reply) { return { foo: "bar", }; }, });
app.post("/", async (request, reply) => { const res = await request.myclient.graphql({ query: "query { movies { title } }", }); return res; });};Telemetry propagation
Section titled “Telemetry propagation”To correctly propagate telemetry information, be sure to get the client from the request object:
fastify.post("/", async (request, reply) => { const res = await request.movies.createMovie({ title: "foo" }); return res;});Errors in Platformatic Client
Section titled “Errors in Platformatic Client”Platformatic Client throws the following errors when an unexpected situation occurs:
PLT_MASSIMO_OPTIONS_URL_REQUIRED=> in your client options, you should provide a validurlPLT_MASSIMO_FORM_DATA_REQUIRED=> you should pass aFormDataobject (fromundicirequest) since you’re doing amultipart/form-datarequestPLT_MASSIMO_MISSING_PARAMS_REQUIRED=> a url path params is missing (and should be added) when doing the client requestPLT_MASSIMO_WRONG_OPTS_TYPE=> a wrong client option type has been passed (and should be properly updated)PLT_MASSIMO_INVALID_RESPONSE_SCHEMA=> response can’t be properly validated due to missing status codePLT_MASSIMO_INVALID_CONTENT_TYPE=> response contains an invalid content typePLT_MASSIMO_INVALID_RESPONSE_FORMAT=> body response doesn’t match with the provided schemaPLT_MASSIMO_UNEXPECTED_CALL_FAILURE=> there has been an unexpected failure when doing the client request