An @imqueue fleet has no public front door of its own, so one service takes
that role: a GraphQL gateway that speaks HTTP outward and typed RPC inward.
This chapter builds the tutorial's API service — scaffolded with
imq service create like any other, then given graphql-yoga over Express — and
resolves its GraphQL fields by calling the User, Auth, Car and TimeTable
services through their generated clients.
The API service exposes an external, HTTP-based interface and orchestrates access to the back-end services we've already built.
GraphQL is an ideal fit for this role — it acts as an orchestrator over the underlying services — which is why we've chosen it here.
This tutorial won't teach GraphQL itself; we'll focus on the @imqueue integration. If you're new to GraphQL, learn it from the official resources first, or skip this part and refer to the finished source code of the API service we built for you on GitHub.
Initializing the service
Although the API service differs in structure from a typical @imqueue service, we can still use @imqueue/cli to scaffold it:
cd ~/my-tutorial-app
imq service create api ./api
This installs everything needed to work with @imqueue. Since we want a GraphQL server over HTTP rather than a classic @imqueue service, we'll add a few more dependencies:
npm i --save express cors graphql graphql-yoga graphql-relay \
graphql-fields-list compression helmet reflect-metadata
npm i --save-dev @types/express @types/cors @types/compression @types/node
NOTE. The exact technology stack here isn't important. You could use Apollo Server instead, or any other solution you prefer. We've picked graphql-yoga over Express with a hand-selected set of add-ons, but this is in no way mandatory.
Now remove ./api/src/Api.ts (we don't need it). The root ./api/index.ts
becomes a thin bootstrap that runs the Application class, and ./api/src/
holds the GraphQL application. We keep all the npm scripts from the @imqueue
boilerplate — they work fine for us — but we change what the service does:
instead of a classic service, it now starts an HTTP server (Express) with a
graphql-yoga endpoint that serves GraphQL requests.
We won't walk through that setup in detail — implement it yourself if you're
comfortable, or refer to the
source code on GitHub. Take a closer
look at
index.ts and
src/Application.ts.
The heart of the @imqueue integration lives in the bootstrapContext() method
of the Application class. There we instantiate all the @imqueue/rpc clients
used to orchestrate requests to the underlying services, and start them as part
of the API service's start-up.
The started clients are then spread into the per-request context that
graphql-yoga builds — together with the user resolved from the X-Auth-User
header — and GraphQL passes that context down to every resolver in the schema,
so any resolver can reach our services whenever it needs to.
import { clientOptions } from '../config.js';
import { user, auth, car, timeTable } from './clients/index.js';
export class Application {
// ...
private static context: {
user: user.UserClient;
auth: auth.AuthClient;
car: car.CarClient;
timeTable: timeTable.TimeTableClient;
};
/**
* Instantiates and starts the @imqueue/rpc clients used to orchestrate
* requests to the back-end services
*/
private static async bootstrapContext(): Promise<void> {
Application.context = {
user: new user.UserClient(clientOptions),
auth: new auth.AuthClient(clientOptions),
car: new car.CarClient(clientOptions),
timeTable: new timeTable.TimeTableClient(clientOptions),
};
await Application.context.user.start();
await Application.context.auth.start();
await Application.context.car.start();
await Application.context.timeTable.start();
}
// ...
}
As we saw in chapter 3, clients are a core part of @imqueue/rpc — they provide the RPC mechanism for calling remote services. There we used a dynamically built client; here we'll build client code statically.
Building the clients
Generating static client code is a good way to work with @imqueue services, because it offers several concrete advantages:
- You don't have to worry about service start-up order — you don't need a service running to build its client.
- You get pre-built code that your IDE can read, so you can explore each service's interface during development, with auto-completion.
- You can version your client code and manage compatibility between versions.
There's one more benefit: with all clients kept in a single place, there's no duplicated client code to maintain. (In larger systems the same client might be generated and used at several network locations, which then requires managing client updates — but our case is simple, so we'll leave that problem aside.)
Generating client code takes a single command per service. Before running it, make sure the target service is up and running:
imq client generate User ./api/src/clients
imq client generate Auth ./api/src/clients
imq client generate Car ./api/src/clients
imq client generate TimeTable ./api/src/clients
You may want to wrap these in an npm script so you can regenerate all clients with a single command, for example:
npm run rebuild-clients
Querying the services
Finally, when building the GraphQL schema, we're ready to query our services. For example, to fetch the list of car brands:
import {
GraphQLResolveInfo,
GraphQLList,
GraphQLSchema,
GraphQLObjectType,
GraphQLString,
} from 'graphql';
import { user, car, timeTable, auth } from '../clients/index.js';
interface Context {
user: user.UserClient;
car: car.CarClient;
timeTable: timeTable.TimeTableClient;
auth: auth.AuthClient;
}
export const schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'Query',
fields: {
brands: {
description: 'Fetches the list of car brands',
type: new GraphQLList(GraphQLString),
async resolve(
source: any,
args: any,
context: Context,
info: GraphQLResolveInfo,
): Promise<string[]> {
try {
return await context.car.brands();
} catch (err) {
console.warn('Fetch brands error:', err);
return [];
}
},
},
},
}),
});
The sample above keeps everything in one file to stay readable. In the finished
service the schema is assembled in
src/schema.ts
from the field definitions under src/queries/ and src/mutations/, the
resolver bodies live in src/helpers/resolvers.ts, and the Context type in
src/types/Context.ts.
Because the clients are started during the API service's start-up and GraphQL exposes them to every resolver through the context, we can call remote services and fetch whatever data we need, right where we need it.
As the example shows, there's very little to do on the client side — just build and use. All the real implementation lives in one place: the service itself.
This lets you write "normal" code, treating your services as ordinary objects with methods, and compose complex combinations of service calls imperatively inside your GraphQL resolvers.
The full API service implementation is available here.
Next up: Deployment.