Writing GraphQL queries
Pastoria uses React Relay for GraphQL data fetching. Relay provides powerful features like automatic query optimization, persisted queries, and seamless server-side rendering integration.
Quick Overview
In Pastoria apps, you write GraphQL queries using the graphql template literal
tag, and consume them with usePreloadedQuery. Queries are preloaded on the
server before rendering, eliminating loading states on initial page load.
Example from examples/starter/src/hello_world.tsx:
import {helloWorld_HelloQuery} from '#genfiles/queries/helloWorld_HelloQuery.graphql.js';
import {EntryPointComponent, graphql, usePreloadedQuery} from 'react-relay';
/**
* @route /hello/:name
* @resource m#hello
* @param {string} name
*/
export const HelloWorldPage: EntryPointComponent<
{nameQuery: helloWorld_HelloQuery},
{}
> = ({queries}) => {
const {greet} = usePreloadedQuery(
graphql`
query helloWorld_HelloQuery($name: String!)
@preloadable
@throwOnFieldError {
greet(name: $name)
}
`,
queries.nameQuery,
);
return (
<div className="flex min-h-screen items-center justify-center">
<div className="text-center">
<h1 className="text-4xl font-bold text-white">{greet}</h1>
</div>
</div>
);
};
Writing Queries
Queries are defined using the graphql template literal:
const data = usePreloadedQuery(
graphql`
query MyComponentQuery($userId: ID!) @preloadable @throwOnFieldError {
user(id: $userId) {
name
email
}
}
`,
queries.myQuery,
);
Key parts:
- Query name: Must be unique across your app (e.g.,
MyComponentQuery) - Variables: Declared with
$prefix and typed (e.g.,$userId: ID!) @preloadable: Required directive for entrypoint queries@throwOnFieldError: Throws errors if any field fails (recommended)
Using usePreloadedQuery
In Pastoria apps, always use usePreloadedQuery to consume queries that were
preloaded by your entrypoint:
export const UserProfile: EntryPointComponent<
{userQuery: UserProfileQuery},
{}
> = ({queries}) => {
const data = usePreloadedQuery(
graphql`
query UserProfileQuery($userId: ID!) @preloadable {
user(id: $userId) {
name
email
avatar
}
}
`,
queries.userQuery, // Preloaded query reference
);
return <div>{data.user.name}</div>;
};
Why usePreloadedQuery?
- ✅ Data is already fetched on the server before rendering
- ✅ No loading states needed for initial render
- ✅ Optimal Core Web Vitals
- ✅ Works seamlessly with SSR and hydration
Don't use useLazyLoadQuery in Pastoria apps—it fetches data on the client
and defeats the purpose of server-side rendering.
Query Structure
Selecting Fields
Request the exact fields you need:
query PostQuery($postId: ID!) @preloadable {
post(id: $postId) {
title
content
publishedAt
}
}
Nested Objects
Query nested objects by selecting their fields:
query PostQuery($postId: ID!) @preloadable {
post(id: $postId) {
title
author {
name
email
}
}
}
Lists
Query lists and map over them in your component:
From examples/nested_entrypoints/src/search_results.tsx:
export const SearchResults: EntryPointComponent<
{citiesQueryRef: searchResults_SearchResultsQuery},
{}
> = ({queries}) => {
const {cities} = usePreloadedQuery(
graphql`
query searchResults_SearchResultsQuery($query: String!)
@preloadable
@throwOnFieldError {
cities(query: $query) {
name
}
}
`,
queries.citiesQueryRef,
);
return (
<div className="grid w-full max-w-lg grid-cols-2">
{cities.map((c) => (
<div key={c.name}>{c.name}</div>
))}
</div>
);
};
Variables
Variables let you pass dynamic values to queries:
query UserPostsQuery($userId: ID!, $limit: Int) @preloadable {
user(id: $userId) {
posts(limit: $limit) {
title
publishedAt
}
}
}
Variables are automatically passed from your route parameters when using
resource-driven entrypoints. Pastoria analyzes your EntryPointComponent types
to detect queries and automatically maps route params to query variables.
Relay Directives
@preloadable
Required for all queries used in entrypoints. Tells Relay this query can be preloaded:
query MyQuery($id: ID!) @preloadable {
# ...
}
@throwOnFieldError
Throws an error if any field in the query fails. Recommended for catching data issues early:
query MyQuery($id: ID!) @preloadable @throwOnFieldError {
# If any field fails, an error is thrown
user(id: $id) {
name
}
}
Without this directive, field errors are silently ignored and return null.
Type Safety
Relay generates TypeScript types for every query. Import and use them for type safety:
import {UserProfileQuery} from '#genfiles/queries/UserProfileQuery.graphql.js';
export const UserProfile: EntryPointComponent<
{userQuery: UserProfileQuery}, // Type-safe query reference
{}
> = ({queries}) => {
const data = usePreloadedQuery(/* ... */, queries.userQuery);
// `data` is fully typed!
console.log(data.user.name); // ✅ TypeScript knows this is a string
};
Compiling Queries
After writing queries, compile them with the Relay compiler:
$ pnpm generate:relay
This:
- Scans your codebase for
graphqltemplate literals - Validates queries against your GraphQL schema
- Generates TypeScript types for query results
- Creates persisted query IDs for production
The generated files appear in __generated__/queries/.
Persisted Queries
Pastoria automatically uses persisted queries, which:
- ✅ Reduce bandwidth by sending query IDs instead of full query text
- ✅ Improve security by only allowing known queries
- ✅ Enable query allowlisting in production
Persisted queries are configured in relay.config.json:
{
"persistConfig": {
"file": "./__generated__/router/persisted_queries.json"
}
}
The generated persisted_queries.json file maps query IDs to query text. No
additional configuration needed—Pastoria handles this automatically.
Query Naming Conventions
Follow these conventions for query names:
- Pattern:
<componentName>_<QueryPurpose>Query - Examples:
UserProfile_UserQueryPostList_PostsQuerysearchResults_SearchResultsQuery
This helps organize queries and avoid naming conflicts.
Common Patterns
Query with Route Parameters
Declare queries in your EntryPointComponent types to automatically pass route
params:
/**
* @route /posts/:postId
* @resource m#post_page
* @param {string} postId
*/
export const PostPage: EntryPointComponent<{postQuery: PostPageQuery}, {}> = ({
queries,
}) => {
const data = usePreloadedQuery(
graphql`
query PostPageQuery($postId: ID!) @preloadable {
post(id: $postId) {
title
content
}
}
`,
queries.postQuery,
);
return <article>{data.post.title}</article>;
};
The $postId variable is automatically populated from the route parameter.
Pastoria detects the postQuery property in your types and maps the postId
route param to the $postId query variable.
Optional Variables
Use nullable types for optional variables:
query SearchQuery($query: String) @preloadable {
# $query can be null/undefined
results(query: $query) {
title
}
}
Match this with optional route params:
/**
* @route /search
* @param {string?} q
*/
Learning More
This is a brief overview focused on Pastoria's usage patterns. For complete Relay documentation on:
- Fragments and data masking
- Mutations
- Subscriptions
- Pagination
- Optimistic updates
- Advanced query features
Visit the official Relay documentation.
Next Steps
Now that you understand queries, learn how to connect them to routes using entrypoints.