The graphql-backend
plugin adds a GraphQL endpoint
(/api/graphql
) to your Backstage instances, and provides a mechanism
to customize it without having to write any bespoke TypeScript.
It uses GraphQL Modules and Envelop plugins so you can compose pieces of schema and middleware from many different places (including other plugins) into a single, complete GraphQL server.
At a minimum, you should install the graphql-backend-module-catalog which adds basic schema elements to access the Backstage Catalog via GraphQL
This approach is suitable for the new Backstage backend system. For the current Backstage plugins system see Backstage Plugins
To install the GraphQL Backend onto your server:
- Add GraphQL plugin and Application backend module in
packages/backend/src/index.ts
:
import { graphqlPlugin } from '@frontside/backstage-plugin-graphql-backend';
const backend = createBackend();
// GraphQL
backend.use(graphqlPlugin());
- Start the backend
yarn workspace example-backend start
This will launch the full example backend. However, without any modules installed, you won't be able to do much with it.
The way to add new types and new resolvers to your GraphQL backend is with GraphQL Modules. These are portable little bundles of schema that you can drop into place and have them extend your GraphQL server. The most important of these that is maintained by the Backstage team is the graphql-backend-module-catalog module that makes your Catalog accessible via GraphQL. Add this module to your backend:
// packages/backend/src/index.ts
import { graphqlModuleCatalog } from '@frontside/backstage-plugin-graphql-backend-module-catalog';
const backend = createBackend();
// GraphQL
backend.use(graphqlPlugin());
backend.use(graphqlModuleCatalog());
To learn more about adding your own modules, see the HydraphQL package.
To extend your schema, you will define it using the GraphQL Schema Definition Language, and then (optionally) write resolvers to handle the various types which you defined.
- Create modules directory where you'll store all your GraphQL modules, for example in
packages/backend/src/modules
- Create a module directory
my-module
there - Create a GraphQL schema file
my-module.graphql
in the module directory
extend type Query {
hello: String!
}
This code adds a hello
field to the global Query
type. Next, we are going to
write a module containing this schema and its resolvers.
- Create a GraphQL module file
my-module.ts
in the module directory
import { resolvePackagePath } from "@backstage/backend-common";
import { loadFilesSync } from "@graphql-tools/load-files";
import { createModule } from "graphql-modules";
export const myModule = createModule({
id: "my-module",
dirname: resolvePackagePath("backend", "src/modules/my-module"),
typeDefs: loadFilesSync(
resolvePackagePath("backend", "src/modules/my-module/my-module.graphql"),
),
resolvers: {
Query: {
hello: () => "world",
},
},
});
- Now we can pass your GraphQL module to GraphQL Application backend module
// packages/backend/src/modules/graphqlMyModule.ts
import { createBackendModule } from "@backstage/backend-plugin-api";
import { graphqlModulesExtensionPoint } from "@frontside/backstage-plugin-graphql-backend-node";
import { MyModule } from "../modules/my-module/my-module";
export const graphqlModuleMyModule = createBackendModule({
pluginId: "graphql",
moduleId: "myModule",
register(env) {
env.registerInit({
deps: { modules: graphqlModulesExtensionPoint },
async init({ modules }) {
await modules.addModules([MyModule]);
},
});
},
});
- And then add it to your backend
// packages/backend/src/index.ts
import { graphqlModuleMyModule } from "./modules/graphqlMyModule";
const backend = createBackend();
// GraphQL
backend.use(graphqlPlugin());
backend.use(graphqlModuleMyModule());
Whereas Graphql Modules are used to extend the schema and resolvers of your GraphQL server, Envelop plugins are used to extend its GraphQL stack with tracing, error handling, context extensions, and other middlewares.
Plugins are be added via declaring GraphQL Yoga backend module.
For example, to prevent potentially sensitive error messages from
leaking to your client in production, add the useMaskedErrors
package.
// packages/backend/src/modules/graphqlPlugins.ts
import { createBackendModule } from '@backstage/backend-plugin-api';
import { graphqlPluginsExtensionPoint } from '@frontside/backstage-plugin-graphql-backend-node';
import { useMaskedErrors } from '@envelop/core';
export const graphqlModulePlugins = createBackendModule({
pluginId: 'graphql',
moduleId: 'plugins',
register(env) {
env.registerInit({
deps: { plugins: graphqlPluginsExtensionPoint },
async init({ plugins }) {
plugins.addPlugins([useMaskedErrors()]);
},
});
},
});
Then add module to your backend:
// packages/backend/src/index.ts
import { graphqlModulePlugins } from './modules/graphqlPlugins';
const backend = createBackend();
// GraphQL
backend.use(graphqlPlugin());
backend.use(graphqlModulePlugins());
The GraphQL context is an object that is passed to every resolver function. It is a convenient place to store data that is needed by multiple resolvers, such as a database connection or a logger.
You can add additional data to the context to GraphQL Yoga backend module:
// packages/backend/src/modules/graphqlContext.ts
import { createBackendModule } from '@backstage/backend-plugin-api';
import { graphqlContextExtensionPoint } from '@frontside/backstage-plugin-graphql-backend-node';
export const graphqlModuleContext = createBackendModule({
pluginId: 'graphql',
moduleId: 'context',
register(env) {
env.registerInit({
deps: { context: graphqlContextExtensionPoint },
async init({ context }) {
context.setContext({ myContext: 'Hello World' });
},
});
},
});
By default, your graphql context will contain a Dataloader
for retrieving
records from the Catalog by their GraphQL ID. Most of the time this is all you
will need. However, sometimes you will need to load data not just from the
Backstage catalog, but from a different data source entirely. To do this, you
will need to pass batch load functions for each data source.
⚠️ Caution! If you find yourself wanting to load data directly from a source other than the catalog, first consider the idea of instead just ingesting that data into the catalog, and then using the default data loader. After consideration, If you still want to load data directly from a source other than the Backstage catalog, then proceed with care.
Load functions are to GraphQL Yoga backend module. Each load function is stored under a unique key which is encoded inside node's id as a data source name
// packages/backend/src/modules/graphqlLoaders.ts
import { createBackendModule } from '@backstage/backend-plugin-api';
import { graphqlLoadersExtensionPoint } from '@frontside/backstage-plugin-graphql-backend-node';
import { NodeQuery } from '@frontside/hydraphql';
export const graphqlModuleLoaders = createBackendModule({
pluginId: 'graphql',
moduleId: 'loaders',
register(env) {
env.registerInit({
deps: { loaders: graphqlLoadersExtensionPoint },
async init({ loaders }) {
loaders.addLoaders({
ProjectAPI: async (
queries: readonly NodeQuery[],
context: GraphQLContext,
) => {
/* Fetch */
},
TaskAPI: async (queries: readonly NodeQuery[], context: GraphQLContext) => {
/* Fetch */
},
});
},
});
},
});
Then you can use @resolve
directive in your GraphQL schemas
interface Node
@discriminates(with: "__source")
@discriminationAlias(value: "Project", type: "ProjectAPI")
@discriminationAlias(value: "Task", type: "TaskAPI")
type Project @implements(interface "Node") {
tasks: [Task] @resolve(at: "spec.projectId", from: "TaskAPI")
}
type Task @implements(interface "Node") {
# ...
}
It's convenient to be able to query the Backstage GraphQL API from inside of Backstage App. You can accomplish this by installing the Backstage GraphiQL Plugin and adding the GraphQL API endpoint to the GraphiQL Plugin API factory.
- Once you installed
@backstage/plugin-graphiql
plugin with these instructions - Modify
packages/app/src/apis.ts
to add your GraphQL API as an endpoint
factory: ({ errorApi, githubAuthApi, discovery }) =>
GraphQLEndpoints.from([
{
id: 'backstage-backend',
title: 'Backstage GraphQL API',
// we use the lower level object with a fetcher function
// as we need to `await` the backend url for the graphql plugin
fetcher: async (params: any) => {
const graphqlURL = await discovery.getBaseUrl('graphql');
return fetch(graphqlURL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
}).then(res => res.json());
},
},
]);
Checkout this example packages/app/src/apis.ts
.
You might want to show the schema from your GraphQL API in the API definition section of an API entity in Backstage. You can use the /api/graphql/schema
endpoint to read the schema provided by your GraphQL API. Here's how:
-
Create the API entity and reference
definition.$text: http://localhost:7007/api/graphql/schema
apiVersion: backstage.io/v1alpha1 kind: API metadata: name: backstage-graphql-api description: GraphQL API provided by GraphQL Plugin spec: type: graphql owner: engineering@backstage.io lifecycle: production definition: $text: http://localhost:7007/api/graphql/schema
-
Modify
app-config.yaml
to allow reading urls fromlocalhost:7007
backend:
...
reading:
allow:
- host: localhost:7007
You might notice that if you have a union
type which is used in
@relation
directive with Connection
type, like this:
union Owner = User | Group
type Resource @implements(interface: "Entity") {
owners: Connection! @relation(name: "ownedBy", nodeType: "Owner")
}
In output schema you'll get:
interface Owner implements Node {
id: ID!
}
type OwnerConnection implements Connection {
pageInfo: PageInfo!
edges: [OwnerEdge!]!
count: Int
}
type OwnerEdge implements Edge {
cursor: String!
node: Owner!
}
type User implements Entity & Node & Owner {
# ...
}
type Group implements Entity & Node & Owner {
# ...
}
The reason why we do that, is because Edge
interface has a node
field with Node
type. So it forces that any object types that
implement Edge
interface must have the node
field with the type
that implements Node
interface. And unions can't implement
interfaces yet
(graphql/graphql-spec#518)
So you just simply can't use unions in such case. As a workaround we
change a union to an interface that implements Node
and each type
that was used in the union, now implements the new interface. To an
end user there is no difference between a union and interface
approach, both variants work similar.