With our Payload CMS persisted query system in place, let's explore how to build a type-safe, efficient frontend integration. We'll start by setting up the necessary tooling and then build our way up to a complete frontend implementation.
First, we need to generate TypeScript types from our GraphQL schema. This is handled by GraphQL Code Generator using several plugins:
// package.json
{
"dependencies": {
"@graphql-codegen/introspection": "^4.0.3",
"graphql-codegen-typescript-client": "0.18.2",
"graphql-codegen-typescript-common": "0.18.2",
"graphql-codegen-typescript-graphql-files-modules": "0.18.2"
}
}
The configuration for type generation is defined in codegen.yml
:
overwrite: true
schema: "http://localhost:3001/api/graphql"
documents: "**/*.graphql"
generates:
lib/generated/graphql.ts:
plugins:
- "typescript"
- "typescript-operations"
- "typescript-graphql-request"
config:
rawRequest: true
inlineFragmentTypes: inline
typesPrefix: CMS
This setup ensures that all our GraphQL operations are properly typed. The typesPrefix
: CMS configuration helps avoid naming conflicts while making it clear where types originate.
At the heart of our frontend implementation is the fetcher system, which handles both development and production environments:
export async function fetcher(identifier: string, variables: any) {
return IS_PRODUCTION
? await persistedFetcher(identifier + ".graphql", variables)
: (await getSdkLogged()[identifier](variables)).data;
}
This fetcher is smart enough to use different strategies based on the environment:
1. In development, it uses the full GraphQL endpoint with detailed logging:
function getSdkLogged(injectedGqlClient?: GraphQLClient): Sdk {
const originalSdk = getSdk(injectedGqlClient ?? gqlClient);
return new Proxy(originalSdk, {
get(target: any, prop: string | symbol) {
if (typeof target[prop] === "function") {
return new Proxy(target[prop], {
apply: (targetMethod, thisArg, argumentsList) => {
const operationName = String(prop);
const startTime = Date.now();
return targetMethod.apply(thisArg, argumentsList).then((result: any) => {
const endTime = Date.now();
const duration = endTime - startTime;
console.log(
chalk.cyan("[GraphQL]"),
chalk.green(operationName.padEnd(20)),
chalk.yellow(`(${duration}ms)`),
);
return result;
});
},
});
}
return target[prop];
},
});
}
2. In production, it uses our persisted query system:
export async function persistedFetcher<T = any>(
operationFile: string,
variables: Record<string, any>,
): Promise<T> {
const startTime = Date.now();
const queryKey = getQueryKey(operationFile);
try {
const response = await fetch(buildUrl(queryKey, variables));
if (!response.ok) {
throw new Error(`GraphQL request failed: ${response.statusText}`);
}
const { data, errors } = await response.json();
if (errors?.[0]) {
throw new Error(errors[0].message);
}
logOperation(queryKey, Date.now() - startTime);
return data;
} catch (error) {
logOperation(
queryKey,
Date.now() - startTime,
error instanceof Error ? error : new Error("Unknown error"),
);
throw error;
}
}
A common challenge in internationalized applications is handling locale formats. Next.js uses the standard "en-US" format, while GraphQL prefers "en_US". We handle this conversion seamlessly:
export const convertCMSLocale = (
string?: LocaleType,
): CMSLocaleInputType & CMSComment_Locale_Input =>
string?.replace("-", "_") as CMSLocaleInputType & CMSComment_Locale_Input;
With our foundation in place, we can build type-safe functions for fetching data. Here's an example for fetching UI strings:
export async function fetchUIStrings(
locale: LocaleType,
ids: string[] = [],
): Promise<Record<string, string>> {
const data = await fetcher("uiStrings", {
locale: convertCMSLocale(locale),
limit: ids.length + 1,
where: {
id: {
in: ids,
},
},
});
return Object.fromEntries(
ZodUiStrings.parse(data.UiStrings?.docs).map((doc) => [doc.id, doc.text]),
);
}
This function showcases several important features:
Type-safe locale conversion
Runtime validation using Zod
Smart limit calculation to catch potential issues early
Clean error handling
Using these functions in Next.js pages becomes straightforward:
export const getStaticProps: GetStaticProps = async ({ locale }) => {
const validatedLocale = validateLocale(locale);
try {
const strings = await fetchUIStrings(validatedLocale, [
"comments-headline",
"comments-error"
]);
return {
props: {
strings
},
revalidate: 60
};
} catch (error) {
// Error handling...
}
};
Our implementation includes comprehensive error handling and logging:
const logOperation = (operationName: string, duration: number, error?: Error): void => {
const status = error ? chalk.red : chalk.green;
const baseLog = [
chalk.cyan("[Persisted GraphQL]"),
status(operationName.padEnd(28)),
chalk.yellow(`(${duration}ms)`),
];
console.log(...baseLog, ...(error ? [chalk.red("ERROR:"), error.message] : []));
};
This logging system provides:
Operation timing information
Color-coded success/failure status
Detailed error messages when things go wrong
Consistent formatting for easy log parsing
In the next part, we'll explore the complete development workflow and advanced developer tooling that makes this system a joy to work with.
Noch keine Kommentare, sei der Erste:
© 2024 von Moritz Thomas