@carvajalconsultants/headstart
v1.0.6
Published
Library to assist in integrating PostGraphile with Tanstack Start and URQL.
Downloads
239
Readme
Headstart
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
Installation
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.tsandpgl.ts. If you need WebSocket support, make sure to add a plugin ingraphile.config.tsas per: https://postgraphile.org/postgraphile/next/subscriptionsInstall Headtart & URQL:
yarn add @carvajalconsultants/headstart urql @urql/exchange-graphcache @urql/exchange-auth- Enable WebSockets in
app.config.tsif 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
/apiendpoint 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 });
}Deployment
- Run
yarn run build cd .output/serverrm -rf node_modulesyarn installyarn run index.mjs
