Manual Entrypoints
Entrypoints are the core routing mechanism in Pastoria. They define how routes map to components and control what data gets preloaded before rendering. Manual entrypoints give you full control over this process.
What is an Entrypoint?
An entrypoint is a configuration object that tells Pastoria:
- What component to render for a route
- What data (GraphQL queries) to preload on the server
- What nested entrypoints to preload for child components
When a user navigates to a route, Pastoria:
- Matches the URL to an entrypoint
- Executes the entrypoint's
getPreloadPropsfunction on the server - Preloads all specified queries and nested entrypoints
- Renders the component with preloaded data
- Sends the fully rendered HTML to the client
- Hydrates the React app on the client with the same preloaded data
This approach eliminates loading spinners for initial page loads and provides optimal Core Web Vitals.
Prerequisites
Before working with entrypoints, you should understand
resources—Pastoria's system for code splitting and lazy
loading. Resources are referenced in entrypoints using
JSResource.fromModuleId().
Manual Entrypoint Structure
A manual entrypoint is defined in a separate .entrypoint.tsx file and exports
an entrypoint constant.
Basic Example
Here's the simplest possible entrypoint from
examples/starter/src/home.entrypoint.tsx:
import {JSResource, ModuleType} from '#genfiles/router/js_resource';
import {EntryPointParams} from '#genfiles/router/router';
import {EntryPoint} from 'react-relay/hooks';
/** @route / */
export const entrypoint: EntryPoint<
ModuleType<'m#home'>,
EntryPointParams<'/'>
> = {
root: JSResource.fromModuleId('m#home'),
getPreloadProps({}) {
return {
queries: {},
};
},
};
Key parts:
@route /: Defines the URL pattern this entrypoint handlesroot: Points to the component resource to render (m#home)getPreloadProps: Function that returns what to preload (currently nothing)
The corresponding home.tsx file defines the resource:
/**
* @resource m#home
*/
export function HomePage() {
return (
<div className="flex min-h-screen items-center justify-center">
<div className="text-center">
<h1 className="text-4xl font-bold text-white">Welcome to Pastoria!</h1>
<p className="mt-4 text-gray-300">
It's like the darkness in the light
</p>
</div>
</div>
);
}
Preloading GraphQL Queries
The real power of entrypoints is preloading GraphQL queries on the server.
Example with Query Preloading
import UserProfileQueryParameters from '#genfiles/queries/UserProfileQuery$parameters';
import {JSResource, ModuleType} from '#genfiles/router/js_resource';
import {EntryPointParams} from '#genfiles/router/router';
import {EntryPoint} from 'react-relay/hooks';
/**
* @route /users/:userId
* @param {string} userId
*/
export const entrypoint: EntryPoint<
ModuleType<'m#user_profile'>,
EntryPointParams<'/users/:userId'>
> = {
root: JSResource.fromModuleId('m#user_profile'),
getPreloadProps({params, schema}) {
const {userId} = schema.parse(params);
return {
queries: {
userQueryRef: {
parameters: UserProfileQueryParameters,
variables: {userId},
},
},
};
},
};
Key concepts:
@param {string} userId: Declares a route parameter with its typeparams: Raw URL parameters passed togetPreloadPropsschema.parse(params): Validates and parses params using generated Zod schemaqueries.userQueryRef: Preloads a query and makes it available to the component asqueries.userQueryRef
The corresponding component consumes the preloaded query:
import {UserProfileQuery} from '#genfiles/queries/UserProfileQuery.graphql.js';
import {EntryPointComponent, graphql, usePreloadedQuery} from 'react-relay';
/**
* @resource m#user_profile
*/
export const UserProfilePage: EntryPointComponent<
{userQueryRef: UserProfileQuery},
{}
> = ({queries}) => {
const data = usePreloadedQuery(
graphql`
query UserProfileQuery($userId: ID!) @preloadable {
user(id: $userId) {
name
email
avatar
}
}
`,
queries.userQueryRef,
);
return (
<div>
<h1>{data.user.name}</h1>
<img src={data.user.avatar} alt={data.user.name} />
<p>{data.user.email}</p>
</div>
);
};
Important: The component must be typed as EntryPointComponent and receive
the queries via props. Use usePreloadedQuery (not useLazyLoadQuery) to
consume the preloaded data.
Nested Entrypoints
Manual entrypoints can preload child entrypoints, enabling progressive loading and code splitting at multiple levels.
Example: Search with Nested Results
From examples/nested_entrypoints/src/search.entrypoint.tsx:
import searchResults_SearchResultsQueryParameters from '#genfiles/queries/searchResults_SearchResultsQuery$parameters';
import {JSResource, ModuleType} from '#genfiles/router/js_resource';
import {EntryPointParams} from '#genfiles/router/router';
import {EntryPoint} from 'react-relay/hooks';
/**
* @route /
* @param {string?} q
*/
export const entrypoint: EntryPoint<
ModuleType<'m#search'>,
EntryPointParams<'/'>
> = {
root: JSResource.fromModuleId('m#search'),
getPreloadProps({params, schema}) {
const {q} = schema.parse(params);
return {
queries: {},
entryPoints: {
searchResults: {
entryPointParams: {},
entryPoint: {
root: JSResource.fromModuleId('m#search_results'),
getPreloadProps({}) {
return {
queries: {
citiesQueryRef: {
parameters: searchResults_SearchResultsQueryParameters,
variables: {query: q ?? ''},
},
},
};
},
},
},
},
};
},
};
The parent component renders the nested entrypoint using EntryPointContainer:
import {ModuleType} from '#genfiles/router/js_resource.js';
import {useRouteParams} from '#genfiles/router/router.jsx';
import {Suspense, useEffect, useState} from 'react';
import {
EntryPoint,
EntryPointComponent,
EntryPointContainer,
} from 'react-relay';
/**
* @resource m#search
*/
export const SearchPage: EntryPointComponent<
{},
{searchResults: EntryPoint<ModuleType<'m#search_results'>>}
> = ({entryPoints}) => {
const {q} = useRouteParams('/');
const [search, setSearch] = useState(q);
return (
<div className="flex min-h-screen flex-col items-center justify-start pt-36">
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search for cities..."
className="min-w-lg mb-24 rounded-lg border border-gray-400 p-4"
/>
<Suspense fallback="Loading...">
<EntryPointContainer
entryPointReference={entryPoints.searchResults}
props={{}}
/>
</Suspense>
</div>
);
};
The child component receives its preloaded queries:
import {searchResults_SearchResultsQuery} from '#genfiles/queries/searchResults_SearchResultsQuery.graphql.js';
import {EntryPointComponent, graphql, usePreloadedQuery} from 'react-relay';
/**
* @resource m#search_results
*/
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 justify-items-center lg:grid-cols-3">
{cities.map((c) => (
<div key={c.name}>{c.name}</div>
))}
</div>
);
};
Benefits of nested entrypoints:
- Progressive code splitting: Load parent component first, child components later
- Data dependency isolation: Child queries only execute after parent logic runs
- Reusable components: Child entrypoints can be used in multiple parent routes
When to Use Manual Entrypoints
Choose manual entrypoints when you need:
- Complex preloading logic: Conditional queries based on parameters
- Nested entrypoints: Parent-child component hierarchies with separate data needs
- Dynamic route behavior: Different queries/components based on runtime conditions
- Full control: You want to explicitly define every aspect of the routing behavior
For simpler routes, consider using resource-driven entrypoints which provide a more concise syntax.
Type Safety
Pastoria generates TypeScript types for all entrypoints:
ModuleType<'m#resource_id'>: Type of the component for a resourceEntryPointParams<'/route/pattern'>: Type-safe route parameters- Zod schemas for runtime parameter validation
When you run pastoria gen, these types are automatically generated based on
your JSDoc annotations.
Summary
Manual entrypoints provide:
- ✅ Full control over route behavior
- ✅ Server-side query preloading for instant page loads
- ✅ Code splitting with lazy-loaded resources
- ✅ Nested entrypoints for progressive loading
- ✅ Type-safe route parameters with runtime validation
- ✅ Optimal performance with React Relay's data fetching
Next, learn about resource-driven entrypoints for a more concise syntax when you don't need manual control.