One of the main reasons we started using GraphQL for data fetching in the Stripe Dashboard was the variety of off-the-shelf tooling available in the community. As we integrated, we discovered opportunities to improve the stack, and some needs that were specific to Stripe. Thankfully, GraphQL also makes it really convenient to build custom tools to fit the specific needs of your project or organization.
In this talk, you'll learn about why GraphQL is uniquely suited to building custom tooling that won’t create a huge maintenance burden. First, I’ll cover some examples of the tools we built for our GraphQL implementation at Stripe. Then, I'll go over how you can build on top of existing libraries including GraphQL.js and graphql-tools. Finally, I'll give a step-by-step guide to building a new custom tool that you could use at your own company—a script that tells you where a certain GraphQL field is used in your frontend code.
Beginners Guide to TikTok for Search - Rachel Pearson - We are Tilt __ Bright...
Building custom GraphQL tooling for your team
1. Building custom GraphQL
tooling for your team
Sashko Stubailo
Engineering Manager, Dashboard Discovery
@stubailo, sashko@stripe.com
2. 1. What has made GraphQL impactful in the
Stripe Dashboard
2. Novel tools we've built internally
3. How you can build your own GraphQL tooling
3. Everything I'm about to present has been a
team effort at Stripe!
It takes a village to create a great
development environment.
4. Developing in the
Stripe Dashboard
• Lots of product teams
contributing at once
• Needs to work together as a
unified whole
5. S T R I P E D A S H B O A R D S TA C K
• React
• Flow
• Jest
• Webpack
• Ruby
• GraphQL and Apollo
6. PA R T 1
What does it take to make
product development
better at scale?
7. P R O D U C T D E V E L O P M E N T
Frontend
API
Backend
8. P R O D U C T D E V E L O P M E N T
Components
API
Backend
State
management
Tests
Backend
Component
explorer
Editor
CI
Monitoring
Type
generation
9. P R O D U C T D E V E L O P M E N T
Components
GraphQL API
Backend
Apollo
State Management
Tests
Backend
Component
explorer
Editor
CI
Monitoring
Type
generation
10. P R O D U C T D E V E L O P M E N T
Components
GraphQL API
Backend
Apollo
State Management
Tests
Backend
Component
explorer
Editor
CI
Monitoring
Type
generation
GQL
GQL
GQL
GQL
GQL
GQL
GQL
12. Main benefit of GraphQL for us:
The wealth of community tools
and content
13. T O O L S W E U S E M O S T LY O F F T H E S H E L F :
• Apollo Client: Simplifies data fetching and
management in React
• GraphQL Ruby: Makes it easy to build a typed API
• Apollo CLI: Static type generation
• graphql-tools: Easy data mocking
15. Why does GraphQL have such a big impact?
The schema defines capabilities
type Author {
id: Int!
firstName: String
lastName: String
posts: [Post]
}
type Post {
id: Int!
title: String
author: Author
votes: Int
}
type Query {
posts: [Post]
author(id: ID!): Author
}
query PostsForAuthor {
author(id: "1") {
firstName
posts {
title
votes
}
}
}
Query describes requirements
16. What makes it possible to build great tools?
• Tooling integrates between frontend and backend
• Can rely on schema being present and correct
• Stable spec and ecosystem means you can build tools once and
they work for a long time
The last point is why it actually makes sense to have internal tooling
around GraphQL!
18. Tools at Stripe: Mocking
Write unit tests and examples without
having to call a real API
• Faster, more resilient tests
• Possible to develop components
before backend is built
• Easy to generate edge case states
Frontend
Fake API
19. GraphQL enables automatic mocking
Because GraphQL is strongly typed, libraries like graphql-tools can
generate correctly-shaped mock results automatically for any query.
Any valid
query
Schema + mocks
for each type
Result of the
correct shape
20. What about edge cases?
A single globally mocked schema is convenient, but isn't well suited
to test specific pathological cases:
• Error states
• Loading states
• Rendering specific values
22. Best of both worlds: Global mocking with overrides
const mocks = {
Todo: () => ({
text: () => faker.sentence(),
completed: () => faker.boolean(),
}),
User: () => ({
name: () => faker.findName()
})
}
const customResolvers = {
Query: () => ({
todoItems: [
{ completed: true },
{ completed: false },
]
})
};
G L O B A L M O C K S O V E R R I D E S
Now, results are automatically filled in from
global mocks, but we can override specific
values for individual tests.
type Todo {
text: String
completed: Boolean
user: User
}
type User {
name: String
}
type Query {
todoItems: [Todo]
}
S C H E M A
23. Example: default mocks
In this example, we're just trying to check if the component renders
correctly. The specific data isn't important, so we don't need to
specify anything.
const wrapper = mount(
<ApolloTestProvider>
<ConnectOverviewPage />
</ApolloTestProvider>,
);
expect(wrapper.find('ConnectLandingPage'))
.toHaveLength(1);
24. Example:
Overriding fields
We want to assert for
specific values in the
rendered component.
We don't want to rely on
values in the global mocks, so
we specify them in the test.
it('renders populated state', async () => {
const customResolvers = {
Merchant: () => ({
balanceSummaries: () => [{
currency: 'usd',
available: 1000,
pending: 2000,
}],
defaultCurrency: () => 'usd',
}),
};
const wrapper = mount(
<ApolloTestProvider
customResolvers={customResolvers}>
<ApolloExample />
</ApolloTestProvider>,
);
await jestHelpers.asyncWait();
const text = wrapper.text();
expect(text).toMatch(/Default currency: usd/);
expect(text).toMatch(/Currency.*Available.*Pending/);
expect(text).toMatch(/$10.*$20/);
});
25. Mock for loading/error states
We've also added helpers for a very common type of edge case:
Errors and loading state.
it('renders loading state', async () => {
const wrapper = mount(
<LoadingProvider>
<ApolloExample />
</LoadingProvider>,
);
await jestHelpers.asyncWait();
expect(wrapper.text())
.toBe('Loading...');
});
it('renders error state', async () => {
const wrapper = mount(
<ErrorProvider>
<ApolloExample />
</ErrorProvider>,
);
await jestHelpers.asyncWait();
expect(wrapper.text())
.toBe('Error! Oh no!');
});
26. GraphQL mocks for prototyping
Designers and developers can combine our component system with
fake data with no additional effort.
27. Mocking overview
✅ Automatic global mocks
✅ Specific overrides to test edge cases
✅ Error states
✅ Loading states
✅ Built in mocks for prototypes
Read the blog post at bit.ly/graphql-mocking
28. Tools at Stripe:
Schema management
• There's one GraphQL schema for the Dashboard
• Anyone at Stripe should be able to independently make changes
• We want to rely on tooling to prevent breakages
29. Our schema.graphql file
encodes the current state of
the schema
It's automatically generated
from the API code and
checked in to Git
30. Detecting breaking
changes in CI
Frontend builds stay in the
wild for a while, so we need to
be carefun about backwards
compatibility.
31. Custom GraphQL tools
• Stable: Built on a standard spec
• Convenient: Leverage community tools like
GraphQL.js, Apollo Codegen, and Babel
• Tailored: Uniquely designed for our team, codebase,
and development environment
33. Let's build a GraphQL field usage finder together
How would we build a tool that searches our codebase for usage of
a specific GraphQL field?
.jsx
.jsx
.jsx
User.name
?
GraphQL
Schema
34. Let's build a GraphQL field usage finder together
How would we build a tool that searches our codebase for usage of
a specific GraphQL field?
1. Get the GraphQL schema
2. Find queries in our codebase
3. Search those queries for usage of the desired field
35. Getting our schema
Super easy using graphql-config. With a .graphqlconfig in your repo:
const { getGraphQLProjectConfig } = require("graphql-config");
const schema = getGraphQLProjectConfig().getSchema();
Accessing it is as easy as:
{
"schemaPath": "src/graphql/schema.graphql"
}
36. Finding GraphQL queries in the codebase
If we're using graphql-tag, we can look for gql template literals:
github.com/apollographql/graphql-tag
const ExampleQuery = gql`
query ExampleQuery {
currentMerchant {
id, defaultCurrency
balanceSummaries {
currency, available, pending
}
}
}
`;
37. Finding GraphQL queries in the codebase
const fs = require("fs");
const { parse } = require("@babel/parser");
// Read a file
const fileContents = fs.readFileSync(filename, { encoding: "utf-8" });
// Create an AST by parsing the file using Babel
const ast = parse(fileContents);
Using Babel to create an Abstract Syntax Tree (AST):
38. Finding GraphQL queries in the codebase
const traverse = require("@babel/traverse").default;
const graphqlStrings = [];
traverse(ast, {
TaggedTemplateExpression: function(path) {
if (path.node.tag.name === "gql") {
graphqlStrings.push(path.node.quasi.quasis[0]);
}
}
});
Looking for TaggedTemplateExpressions named "gql":
40. Searching for fields in each query
const { parse } = require("graphql");
const ast = parse(jsNode.value.raw);
Parsing the GraphQL query:
41. Searching for fields in each query
const { visit, visitWithTypeInfo, TypeInfo } = require("graphql");
const typeInfo = new TypeInfo(schema);
visit(ast, visitWithTypeInfo(typeInfo, {
Field(graphqlNode) {
const fieldName = typeInfo.getParentType().name +
"." + graphqlNode.name.value;
// Now, check if it's the field we're looking for
}
}));
Looking through all the fields:
42. Searching for fields in each query
if (fieldName === fieldWeAreLookingFor) {
const operationName = ast.definitions[0].name.value,
const line = jsNode.loc.start.line;
console.log(`${filename}:${line} ${operationName}`);
}
Compare to the field we're looking for and print results:
43. Searching for fields in each query
const { parse, visit, visitWithTypeInfo, TypeInfo } = require("graphql");
const ast = parse(jsNode.value.raw);
const typeInfo = new TypeInfo(schema);
visit(ast, visitWithTypeInfo(typeInfo, {
Field(graphqlNode) {
const fieldName = typeInfo.getParentType().name + "." + graphqlNode.name.value;
if (fieldName === fieldWeAreLookingFor) {
const operationName = ast.definitions[0].name.value,
const line = jsNode.loc.start.line;
console.log(`${filename}:${line} ${operationName}`);
}
}
}));
The whole GraphQL part:
44. Putting it all together
Once we add some plumbing to read in files and accept an
argument, it looks like this:
$ node graphql-field-finder.js Query.user
src/components/customers/CardsSection.js:40 AllCardsQuery
src/graphql/queries/UnauthedClientsQuery.js:13 UnauthedClientsQuery
src/user_settings/TwoFactorAuthenticationSettings.js:46 TwoFactorSettingsQuery
tests/functional/full_flows_test.jsx:401 UnauthedClientsQuery
tests/lib/ApolloTestProvider.test.jsx:13 ApolloTestProviderQuery
tests/lib/ApolloTestProvider.test.jsx:49 ApolloTestProviderUsernameQuery
45. R E C A P : W H AT W E U S E D
• graphql-config to get the schema
• graphql-tag to identify queries
• Babel to extract queries from code
• GraphQL.js to find fields in queries
See the code at bit.ly/graphql-field-finder
46. Building custom GraphQL tooling for your team
Sashko Stubailo
@stubailo, sashko@stripe.com
We're hiring at Stripe in Dublin, SF, Singapore,
Seattle, and remote in North America!
Go forth and build your own GraphQL tools to make product
development better at your company!
Get this presentation: bit.ly/custom-graphql-tooling