Library to assist in integrating PostGraphile with Tanstack Start and URQL.
This library is made up of:
- An URQL Exchange that queries grafast for SSR pages (so we don't make an unecessary HTTP request)
- A Grafserv adapter to work with Tanstack Start, including Web Socket support.
- SSR Exchange to be used on the server and client for hydration
-
Initialize Tanstack Start as per: https://tanstack.com/router/latest/docs/framework/react/start/getting-started
-
Install Postgraphile as a library as per: https://postgraphile.org/postgraphile/next/quick-start-guide#install-postgraphile (ensure to use the Typescript files). Specifically, the
graphile.config.ts
andpgl.ts
. If you need WebSocket support, make sure to add a plugin ingraphile.config.ts
as per: https://postgraphile.org/postgraphile/next/subscriptions -
Install Headtart & URQL:
yarn add @carvajalconsultants/headstart urql @urql/exchange-graphcache @urql/exchange-auth
- Enable WebSockets in
app.config.ts
if you need it:
// app.config.ts
import { defineConfig } from "@tanstack/react-start/config";
export default defineConfig({
server: {
experimental: {
websocket: true,
},
},
});
- Make sure to add the
/api
endpoint to the grafserv configuration so that Ruru GraphQL client works correctly:
// graphile.config.ts
const preset: GraphileConfig.Preset = {
...
grafserv: {
...
graphqlPath: "/api",
eventStreamPath: "/api",
},
...
};
- Add the api.ts file so that it calls our GraphQL handler. This will receive all GraphQL requests at the /api endpoint.
// app/api.ts
import { defaultAPIFileRouteHandler } from "@tanstack/react-start/api";
import { createStartAPIHandler } from "@carvajalconsultants/headstart/server";
import { pgl } from "../pgl";
export default createStartAPIHandler(pgl, defaultAPIFileRouteHandler);
- Now we need to configure URQL for client and server rendering, first we start with the server. Create this provider:
// app/graphql/serverProvider.tsx
import { Client } from "urql";
import { grafastExchange } from "@carvajalconsultants/headstart/server";
import { ssr } from "@carvajalconsultants/headstart/client";
import { pgl } from "../../pgl";
/**
* Configure URQL for server side querying with Grafast.
*
* This removes the need to make an HTTP request to ourselves and simply executes the GraphQL query.
*/
export const client = new Client({
url: ".",
exchanges: [ssr, grafastExchange(pgl)],
});
- Now the client side for URQL:
// app/graphql/clientProvider.tsx
import { ssr } from "@carvajalconsultants/headstart/client";
import { authExchange } from "@urql/exchange-auth";
import { cacheExchange } from "@urql/exchange-graphcache";
//import { relayPagination } from "@urql/exchange-graphcache/extras";
import { Client, fetchExchange } from "urql";
/**
* Creates an authentication exchange for handling secure GraphQL operations.
* This exchange ensures that all GraphQL requests are properly authenticated
* and handles authentication failures gracefully.
*
* @returns {Object} Authentication configuration object
* @returns {Function} .addAuthToOperation - Prepares operations with auth context
* @returns {Function} .didAuthError - Detects authentication failures after server request
* @returns {Function} .refreshAuth - Handles auth token refresh
*/
const auth = authExchange(async () => {
//TODO Implement authentication checking for the client s ide
await new Promise((resolve) => {
setTimeout(() => {
console.log("IMPLEMENT CLIENT AUTH CHECK");
resolve();
}, 2000);
});
return {
/**
* Processes each GraphQL operation to include authentication context.
* Currently configured as a pass-through as tokens are in cookies.
*
* @param {Operation} operation - The GraphQL operation to authenticate
* @returns {Operation} The operation with authentication context
*/
addAuthToOperation: (operation) => operation,
/**
* Identifies when an operation has failed due to authentication issues.
* Used to trigger authentication refresh flows when needed.
*
* @param {Error} error - The GraphQL error response
* @returns {boolean} True if the error was caused by authentication failure
*/
didAuthError: (error) => error.graphQLErrors.some((e) => e.extensions?.code === "FORBIDDEN"),
/**
* Handles refreshing authentication when it becomes invalid.
* Currently implemented as a no-op as token refresh is handled by getSession().
*/
refreshAuth: async () => {
/* No-op, this is done in getSession() */
},
};
});
/**
* Configured GraphQL client for the application.
* Provides a centralized way to make authenticated GraphQL requests with
* proper caching and server-side rendering support.
*/
export const client = new Client({
url: "http://localhost:3000/api/graphql",
exchanges: [
cacheExchange({
resolvers: {
Query: {
// Implements relay-style pagination for fills pending match
//queryName: relayPagination(),
},
},
}),
auth,
ssr,
fetchExchange,
],
});
- Create the server side router which uses Grafast to execute queries:
// app/serverRouter.tsx
import { ssr } from "@carvajalconsultants/headstart/client";
import { createRouter as createTanStackRouter } from "@tanstack/react-router";
import { Provider } from "urql";
import { client } from "./graphql/serverProvider";
import { routeTree } from "./routeTree.gen";
import type { ReactNode } from "react";
export function createRouter() {
const router = createTanStackRouter({
routeTree,
context: {
client,
},
// Send data to client so URQL can be hydrated.
dehydrate: () => ({ initialData: ssr.extractData() }),
// Wrap our entire route with the URQL provider so we can execute queries and mutations.
Wrap: ({ children }: { children: ReactNode }) => <Provider value={client}>{children}</Provider>,
});
return router;
}
- Modify the TSR server-side rendering function to use this new router:
/* eslint-disable */
// app/ssr.tsx
/// <reference types="vinxi/types/server" />
import { getRouterManifest } from "@tanstack/react-start/router-manifest";
import {
createStartHandler,
defaultStreamHandler,
} from "@tanstack/start/server";
import { createRouter } from "./serverRouter";
export default createStartHandler({
createRouter,
getRouterManifest,
})(defaultStreamHandler);
- Add the client side router which uses the fetch exchange to execute queries, mutations, etc.:
// app/clientRouter.tsx
import { ssr } from "@carvajalconsultants/headstart/client";
import { createRouter as createTanStackRouter } from "@tanstack/react-router";
import { Provider } from "urql";
import { client } from "./graphql/clientProvider";
import { routeTree } from "./routeTree.gen";
import type { ReactNode } from "react";
import type { SSRData } from "urql";
export function createRouter() {
const router = createTanStackRouter({
routeTree,
context: {
client,
},
hydrate: (dehydrated) => {
// Hydrate URQL with data passed by TSR, this is generated by dehydrate function in server router.
ssr.restoreData(dehydrated.initialData as SSRData);
},
// Wrap our entire route with the URQL provider so we can execute queries and mutations.
Wrap: ({ children }: { children: ReactNode }) => <Provider value={client}>{children}</Provider>,
});
return router;
}
- Tell TSR to use our client side router:
// app/client.tsx
/// <reference types="vinxi/types/client" />
import { StartClient } from "@tanstack/start";
import { hydrateRoot } from "react-dom/client";
import { createRouter } from "./clientRouter";
const router = createRouter();
hydrateRoot(document.getElementById("root")!, <StartClient router={router} />);
- Last but not least, you're ready to start using URQL on your components and pages. First we create the route using the loader option so we can pre-load data:
export const Route = createFileRoute("/")({
...
validateSearch: zodSearchValidator(paramSchema),
loaderDeps: ({ search: { page } }) => ({ page }),
loader: ({ context, deps: { page } }) =>
context.client.query(
gql`...`
{ first: CHARITIES_PER_PAGE, offset: (page - 1) * CHARITIES_PER_PAGE },
),
...
});
- Now in your component, you can query with URQL as you normally would:
const Home = () => {
const { page } = Route.useSearch();
const [{ data, error }] = useQuery({
query: gql`...`,
variables: {
first: CHARITIES_PER_PAGE,
offset: (page - 1) * CHARITIES_PER_PAGE,
},
});
// Subscribe to any data changes on the server
useSubscription({ query: allCharitiesSubscription });
}
- Run
yarn run build
cd .output/server
rm -rf node_modules
yarn install
yarn run index.mjs