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 validurl
PLT_CLIENT_FORM_DATA_REQUIRED
=> you should pass aFormData
object (fromundici
request) since you're doing amultipart/form-data
requestPLT_CLIENT_MISSING_PARAMS_REQUIRED
=> a url path params is missing (and should be added) when doing the client requestPLT_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 codePLT_CLIENT_INVALID_CONTENT_TYPE
=> response contains an invalid content typePLT_CLIENT_INVALID_RESPONSE_FORMAT
=> body response doesn't match with the provided schemaPLT_CLIENT_UNEXPECTED_CALL_FAILURE
=> there has been an unexpected failure when doing the client request