Improving Code Readability Using GraphQL Fragment Colocation

Search for a command to run...

No comments yet. Be the first to comment.
TL;DR 🛒 If your Shopify cart attributes are empty in /cart.js or missing on the order page: Add the input with name="attributes[your-key]" on the cart page Check saved values at {your-store-url}/ca

I didn't think I could ship a web app within a week. Then something happened on a train that changed that. This is the story of how I built and shipped Roka — and what AI actually made possible. The

Intro I set up Biome for a small Next.js app and felt great about it. Then I realized a few days later that the class name sorting I'd configured wasn't doing anything - and the reason was... embarras
Creating Shopify Theme with AI - #4

Creating Shopify Theme with AI - #3

At my job, I worked on a Next.js application that used GraphQL. We also used GraphQL codegen for TypeScript types.
Initially, a senior developer with a mobile development background introduced a module-based pattern, which led us to create models for each GraphQL type.
This is a simplified version of the UserModel:
export class UserModel implements User {
public readonly id: Scalars['ID'];
public readonly firstName: Scalars['String'];
public readonly lastName: Scalars['String'];
constructor(user: User | undefined) {
this.id = user?.id ?? '';
this.firstName = user?.firstName ?? '';
this.lastName = user?.lastName ?? '';
}
get fullName(): string {
return `${this.firstName} ${this.lastName}`;
}
}
This UserModel implements auto generated User type. (So eventually includes all user tables fields.) And defined some private methods.
And for the GraphQL, we kept queries in a single folder:
// src/graphql/queries/user.graphql
query User {
user {
...UserDetails
}
}
fragment UserDetails {
id
firstName
lastName
email
address
phoneNumber
# Fetching all fields
}
We were using fragments but read all fields to make it easier to reference them in the models.
And this is how we refer to user’s full name:
const user = new UserModel(graphql_result);
user.fullName();
For this example, using only firstName and lastName is straightforward. However, in real applications, models tend to be more complex.
Since we were querying all fields and including them in the models, we were unsure which properties were necessary in different parts of the app. As a result, we often ended up over-fetching data, which negated the benefits of GraphQL. 🫣
After a few years, the original project was canceled. However, I got the opportunity to build a new app from scratch with the same tech stack. The senior developer had left, and I became the lead front-end developer. 😎 This time, I decided to get rid of the module-based pattern entirely.
Initially, I wrote helper functions:
// src/helpers/user.ts
const getFullName = (user: User) => {
return `${user.firstName} ${user.lastName}`;
};
However, I still kept using the generated User type. Also, GraphQL queries are still in a single folder, leading to the same inefficiencies.
Then, I got a advisor who introduced me to the power of GraphQL fragment colocation. ✨
GraphQL fragment colocation is the practice of defining fragments near the components or functions that consume them. Instead of keeping all GraphQL queries in a centralized location, colocating fragments ensures that only the necessary fields are queried and makes the code more modular and maintainable.
For more details, check out:
We decided on new rules based on my advisor’s advice.
New Coding Rules are:
Write fragments in the same file as the functions/components using them
Query only the fields needed in that file
Avoid using generated types directly
Example:
// src/helpers/user.tsx
export const USER_FULLNAME = graphql(`
fragment UserFullName on User {
firstName
lastName
middleName
}
`);
export const getFullName = (user: {
firstName: string;
lastName: string;
middleName: string;
}) => {
return `${user.firstName} ${user.middleName} ${user.lastName}`;
};
Now, the helper function defines its required fields explicitly via a fragment. This makes it clear which properties are used and avoids unnecessary data fetching.
Or we could use DocumentType which is generated from codegen:
import type { DocumentType } from "~/__generated__";
export const getFullName = (user: DocumentType<typeof USER_FULLNAME>) => {
return `${user.firstName} ${user.middleName} ${user.lastName}`;
};
* Note that we are not using User type here.
When calling the function, we ensure the required data is passed:
// src/page/user.tsx
import { getFullName } from '../helpers/user';
const { data } = useQuery(
graphql(`
query UserFullNameQuery {
user {
...UserFullName
}
}
`)
);
const fullName = getFullName(data.user);
By colocating GraphQL fragments and only fetching necessary fields, we achieve:
Improved Readability: Queries are easier to understand and maintain.
Reduced Over-fetching: We fetch only the data we actually use.
Better Maintainability: Changes to data structures remain localized.
This approach makes GraphQL development cleaner and more efficient, ensuring that our frontend remains both performant and easy to work with. 🥳