Skip to main content
Version: Next

Platformatic Client

Create a Fastify plugin that exposes a client for a remote OpenAPI or GraphQL API.

Creating a Client

OpenAPI Client

To create a client for a remote OpenAPI API, use the following command:

$ platformatic client http://example.com/to/schema/file --name myclient

GraphQL Client

To create a client for a remote Graphql API, use the following command:

$ platformatic client http://example.com/grapqhl --name myclient

Forcing Client Type

If the Platformatic app supports both OpenAPI and GraphQL, the OpenAPI client will be the one generated by default. To force the generation of a specific client, pass the --type <openapi | graphql> parameter.

$ platformatic client http://example.com/to/schema/file --name myclient --type graphql

Usage with Platformatic Service or Platformatic DB

Running the generator in a Platformatic application automatically extends it to load your client by editing the configuration file and adding a clients section.

Example Usage in JavaScript (GraphQL)

Use the client in your JavaScript application, by calling a GraphQL endpoint:

// Use a typescript reference to set up autocompletion
// and explore the generated APIs.

/// <reference path="./myclient" />

/** @type {import('fastify').FastifyPluginAsync<{} */
module.exports = async function (app, opts) {
app.post('/', async (request, reply) => {
const res = await request.myclient.graphql({
query: 'query { movies { title } }'
})
return res
})
}

Example Usage in TypeScript (OpenAPI)

Use the client in Typescript application, by calling an OpenAPI endpoint:

import { FastifyInstance } from 'fastify'
/// <reference path="./myclient" />

export default async function (app: FastifyInstance) {
app.get('/', async (request, reply) => {
return requests.myclient.get({})
})
}

Client Configuration Example

The client configuration in the platformatic.json would look like this:

{
"clients": [{
"schema": "./myclient/myclient.openapi.json" // or ./myclient/myclient.schema.graphl
"name": "myclient",
"type": "openapi" // or graphql
"url": "{ PLT_MYCLIENT_URL }"
}]
}

Note that the generator would also have updated the .env and .env.sample files if they exist.

Generating a client for a service running within Platformatic Runtime

Platformatic Runtime allows you to create a network of services that are not exposed. To create a client to invoke one of those services from another, run:

$ platformatic client --name <clientname> --runtime <serviceId>

Where <clientname> is the name of the client and <serviceId> is the id of the given service (which correspond in the basic case with the folder name of that service). The client generated is identical to the one in the previous section.

Note that this command looks for a platformatic.json in a parent directory.

Example

As an example, consider a network of three microservices:

  • somber-chariot, an instance of Platformatic DB;
  • languid-noblemen, an instance of Platformatic Service;
  • pricey-paesant, an instance of Platformatic Composer, which is also the runtime entrypoint.

From within the languid-noblemen folder, we can run:

$ platformatic client --name chariot --runtime somber-chariot

The client configuration in the platformatic.json would look like:

{
"clients": [{
"path": "./chariot",
"serviceId": "somber-chariot"
}]
}

Even if the client is generated from an HTTP endpoint, it is possible to add a serviceId property each client object shown above. This is not required, but if using the Platformatic Runtime, the serviceId property will be used to identify the service dependency.

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

$ platformatic client http://example.com/to/schema/file --name myclient --types-only

This will create the single myclient.d.ts file.

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 all the individual Request and Reponse payloads 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>;
}

type ClientPlugin = FastifyPluginAsync<NonNullable<client.ClientOptions>>

declare module 'fastify' {
interface FastifyInstance {
'client': Client;
}

interface FastifyRequest {
'client': Client;
}
}

declare namespace Client {
export interface ClientOptions {
url: string
}
export const client: ClientPlugin;
export { client as default };
}

declare function client(...params: Parameters<ClientPlugin>): ReturnType<ClientPlugin>;
export = client;

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:

declare module 'fastify' {
interface GraphQLQueryOptions {
query: string;
headers: Record<string, string>;
variables: Record<string, unknown>;
}
interface GraphQLClient {
graphql<T>(GraphQLQuery): PromiseLike<T>;
}
interface FastifyInstance {
'client'
: GraphQLClient;

}

interface FastifyRequest {
'client'<T>(GraphQLQuery): PromiseLike<T>;
}
}

declare namespace client {
export interface Clientoptions {
url: string
}
export interface Movie {
'id'?: string;

'title'?: string;

'realeasedDate'?: 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;

}
export const client: Clientplugin;
export { client as default };
}

declare function client(...params: Parameters<Clientplugin>): ReturnType<Clientplugin>;
export = client;

Given only you can know what GraphQL query you are producing, you are responsible for typing it accordingly.

Usage with Standalone Fastify

If a platformatic configuration file is not found, a complete Fastify plugin is generated to be used in your Fastify application like this:

const fastify = require('fastify')()
const client = require('./your-client-name')

fastify.register(client, {
url: 'http://example.com'
})

// GraphQL
fastify.post('/', async (request, reply) => {
const res = await request.movies.graphql({
query: 'mutation { saveMovie(input: { title: "foo" }) { id, title } }'
})
return res
})

// OpenAPI
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 @platformatic/client as a dependency.

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

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

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

Platformatic Client throws the following errors when an unexpected situation occurs:

  • PLT_CLIENT_OPTIONS_URL_REQUIRED => in your client options, you should provide a valid url
  • PLT_CLIENT_FORM_DATA_REQUIRED => you should pass a FormData object (from undici request) since you're doing a multipart/form-data request
  • PLT_CLIENT_MISSING_PARAMS_REQUIRED => a url path params is missing (and should be added) when doing the client request
  • PLT_CLIENT_WRONG_OPTS_TYPE => a wrong client option type has been passed (and should be properly updated)
  • PLT_CLIENT_INVALID_RESPONSE_SCHEMA => response can't be properly validated due to missing status code
  • PLT_CLIENT_INVALID_CONTENT_TYPE => response contains an invalid content type
  • PLT_CLIENT_INVALID_RESPONSE_FORMAT => body response doesn't match with the provided schema
  • PLT_CLIENT_UNEXPECTED_CALL_FAILURE => there has been an unexpected failure when doing the client request