The 5Stack GraphQL API provides type-safe data access with real-time subscriptions, powered by Hasura GraphQL Engine and Apollo Client.
Architecture Overview
Hasura Engine GraphQL API over PostgreSQL with real-time subscriptions
Apollo Client GraphQL client with caching, subscriptions, and error handling
GraphQL Zeus Type-safe query builder and code generator
GraphQL Zeus
Zeus generates TypeScript types and a type-safe query builder from the GraphQL schema.
Code Generation
Types are generated from the Hasura schema:
# package.json script
zeus https:// $NUXT_PUBLIC_API_DOMAIN /v1/graphql ./generated --ts --td \
--header=x-hasura-admin-secret: $HASURA_GRAPHQL_ADMIN_SECRET
The --td flag generates typed document nodes for use with Apollo Client.
Type-Safe Selectors
Zeus provides a Selector API for building reusable field selections:
// graphql/playerFields.ts
import { Selector } from "@/generated/zeus" ;
export const playerFields = Selector ( "players" )({
steam_id: true ,
name: true ,
avatar_url: true ,
country: true ,
elo: true ,
role: true ,
status: true ,
});
// graphql/meGraphql.ts
import { Selector } from "@/generated/zeus" ;
import { playerFields } from "./playerFields" ;
export const meFields = Selector ( "players" )({
... playerFields , // Extend base fields
name_registered: true ,
role: true ,
profile_url: true ,
matchmaking_cooldown: true ,
current_lobby_id: true ,
language: true ,
country: true ,
teams: [
{},
{
id: true ,
name: true ,
short_name: true ,
role: true ,
},
],
});
Query Generation
Helper functions wrap Zeus queries with graphql-tag:
// graphql/graphqlGen.ts
import gql from "graphql-tag" ;
import { Zeus } from "@/generated/zeus" ;
import type { ValueTypes , OperationOptions , ScalarDefinition } from "@/generated/zeus" ;
export function generateQuery < Z extends ValueTypes [ O ], O extends "query_root" >(
query : Z | ValueTypes [ O ],
operationOptions ?: OperationOptions ,
scalars ?: ScalarDefinition ,
) {
return gql ( Zeus ( "query" , query , { operationOptions , scalars }));
}
export function generateMutation < Z extends ValueTypes [ O ], O extends "mutation_root" >(
mutation : Z | ValueTypes [ O ],
operationOptions ?: OperationOptions ,
scalars ?: ScalarDefinition ,
) {
return gql ( Zeus ( "mutation" , mutation , { operationOptions , scalars }));
}
export function generateSubscription < Z extends ValueTypes [ O ], O extends "query_root" >(
subscription : Z | ValueTypes [ O ],
operationOptions ?: OperationOptions ,
scalars ?: ScalarDefinition ,
) {
return gql ( Zeus ( "subscription" , subscription , { operationOptions , scalars }));
}
Apollo Client Configuration
The Apollo Client is configured in a Nuxt plugin with support for both HTTP and WebSocket transports.
Plugin Setup
// plugins/apollo.client.ts
import { onError } from "@apollo/client/link/error" ;
import { RetryLink } from "@apollo/client/link/retry" ;
import { GraphQLWsLink } from "@apollo/client/link/subscriptions" ;
import { createClient } from "graphql-ws" ;
import { createHttpLink , from , split } from "@apollo/client/core" ;
export default defineNuxtPlugin (( nuxtApp ) => {
const $apollo = nuxtApp . $apollo ;
const config = useRuntimeConfig ();
// Error handling link
const errorLink = onError (( error ) => {
nuxtApp . callHook ( 'apollo:error' , error );
});
// Retry logic for failed requests
const retryLink = new RetryLink ({
delay: {
initial: 300 ,
max: 60000 ,
jitter: true ,
},
attempts : ( count , operation , e ) => {
if ( e ?. response ?. status === 401 ) return false ;
return count < 30 ;
},
});
// HTTP link for queries and mutations
const httpLink = createHttpLink ({
credentials: 'include' ,
uri: `https:// ${ config . public . apiDomain } /v1/graphql` ,
});
// WebSocket link for subscriptions
const wsClient = createClient ({
url: `wss:// ${ config . public . apiDomain } /v1/graphql` ,
connectionParams: {
credentials: 'include' ,
},
});
nuxtApp . provide ( 'wsClient' , wsClient );
const wsLink = new GraphQLWsLink ( wsClient );
// Split traffic between HTTP and WS based on operation
const splitLink = split (
({ query }) => {
const definition = getMainDefinition ( query );
return (
definition . kind === 'OperationDefinition' &&
definition . operation === 'subscription'
);
},
wsLink ,
httpLink ,
);
$apollo . defaultClient . setLink ( from ([ errorLink , retryLink , splitLink ]));
} ) ;
Credentials : Cookies are included in all requests for authentication
Retry Logic : Failed requests retry up to 30 times with exponential backoff
Error Handling : Errors trigger a global hook for toast notifications
Transport Splitting : Subscriptions use WebSocket, queries/mutations use HTTP
Cache Configuration
$apollo . defaultClient . defaultOptions = {
watchQuery: {
fetchPolicy: 'cache-and-network' , // Show cached data, then fetch fresh
},
query: {
fetchPolicy: 'network-only' , // Always fetch fresh data
},
};
Querying Data
Helper Function
A helper provides easy access to the Apollo Client:
// graphql/getGraphqlClient.ts
export default function getGraphqlClient () {
const { clients } = useApollo ();
if ( ! clients ?. default ) {
throw Error ( "missing client" );
}
return clients . default ;
}
Running Queries
import getGraphqlClient from '~/graphql/getGraphqlClient' ;
import { generateQuery } from '~/graphql/graphqlGen' ;
import { $ } from '~/generated/zeus' ;
const { data } = await getGraphqlClient (). query ({
query: generateQuery ({
players: [
{
where: {
steam_id: {
_in: $ ( 'steam_ids' , '[bigint]!' ),
},
},
},
playerFields ,
],
}),
variables: {
steam_ids: [ '76561198012345678' ],
},
});
Variables with Type Safety
Zeus’s $ function creates typed variables:
import { $ } from '~/generated/zeus' ;
// Define a variable with name and type
const steamIdVar = $ ( 'steam_id' , 'bigint!' );
// Use in query
const query = generateQuery ({
players_by_pk: [
{ steam_id: steamIdVar },
playerFields ,
],
});
Subscriptions
Real-time subscriptions use the same API as queries:
// Example from stores/AuthStore.ts
function subscribeToMe ( steam_id : string , callback : () => void ) {
const subscription = getGraphqlClient (). subscribe ({
query: generateSubscription ({
players_by_pk: [
{ steam_id: $ ( 'steam_id' , 'bigint!' ) },
meFields ,
],
}),
variables: { steam_id },
});
subscription . subscribe ({
next : ({ data }) => {
me . value = data ?. players_by_pk ;
callback ();
},
error : ( error ) => {
console . error ( 'Subscription error:' , error );
},
});
}
Subscriptions automatically reconnect if the WebSocket connection is lost.
Mutations
Mutations use typed document nodes for full type safety:
import { typedGql } from '~/generated/zeus/typedDocumentNode' ;
const { data } = await getGraphqlClient (). mutate ({
mutation: typedGql ( 'mutation' )({
insert_lobbies_one: [
{
object: {},
},
{
id: true ,
},
],
}),
});
const lobbyId = data . insert_lobbies_one . id ;
Complex Queries
Zeus supports complex nested queries with aggregations:
// graphql/matchLineupsGraphql.ts (excerpt)
import { $ , e_utility_types_enum , order_by , Selector } from "~/generated/zeus" ;
export const matchLineups = Selector ( "match_lineups" )({
id: true ,
name: true ,
lineup_players: [
{
order_by: [
{ captain: order_by . desc_nulls_last },
{ player: { name: $ ( 'order_by_name' , 'order_by!' ) } },
],
},
{
captain: true ,
steam_id: true ,
player: {
... playerFields ,
// Aggregated stats
kills_aggregate: [
{
where: {
match_id: { _eq: $ ( 'matchId' , 'uuid!' ) },
team_kill: { _eq: false },
},
},
{
aggregate: [{}, { count: true }],
},
],
deaths_aggregate: [
{
where: { match_id: { _eq: $ ( 'matchId' , 'uuid!' ) } },
},
{
aggregate: [{}, { count: true }],
},
],
// Nested aggregation with aliases
__alias: {
hs_kills_aggregate: {
kills_aggregate: [
{
where: {
match_id: { _eq: $ ( 'matchId' , 'uuid!' ) },
headshot: { _eq: true },
},
},
{ aggregate: [{}, { count: true }] },
],
},
},
},
},
],
});
Advanced Features Demonstrated
Nested Aggregations : Count kills, deaths, assists within players
Conditional Filtering : Filter aggregations with where clauses
Field Aliases : Use __alias to query the same field with different filters
Sorting : Multi-level order_by on nested relations
Variables : Reusable $matchId variable throughout the query
Error Handling
Global error handling shows user-friendly messages:
nuxtApp . hook ( 'apollo:error' , ( error ) => {
if ( error . graphQLErrors ) {
for ( const graphqlError of error . graphQLErrors ) {
// Skip auth errors (handled separately)
if ([
'Unauthorized' ,
'webhook authentication request' ,
'Invalid response from authorization hook' ,
]. includes ( graphqlError . message )) {
return ;
}
// Show error toast
toast ({
variant: 'destructive' ,
title: nuxtApp . $i18n . t ( 'common.error' ),
description: graphqlError . message ,
});
}
}
});
Type Safety
The entire GraphQL stack provides end-to-end type safety:
import type { InputType , GraphQLTypes } from '~/generated/zeus' ;
// Infer types from selectors
type Player = InputType < GraphQLTypes [ 'players' ], typeof playerFields >;
type Me = InputType < GraphQLTypes [ 'players' ], typeof meFields >;
// Fully typed in stores
export const useAuthStore = defineStore ( 'auth' , () => {
const me = ref < Me >(); // Type-safe player object
// Return type inferred from selector
return { me };
});
Best Practices
Define field selections once and reuse them: // Good: Reusable selector
export const playerFields = Selector ( 'players' )({ /* fields */ });
// Use everywhere
generateQuery ({ players: [{}, playerFields ] });
Use descriptive variable names with types: // Good
const matchId = $ ( 'matchId' , 'uuid!' );
const steamIds = $ ( 'steam_ids' , '[bigint]!' );
// Avoid
const id = $ ( 'id' , 'uuid!' );
Always unsubscribe when components unmount: const subscription = getGraphqlClient (). subscribe ( /* ... */ );
const unsubscribe = subscription . subscribe ( /* ... */ );
onUnmounted (() => {
unsubscribe . unsubscribe ();
});
Next Steps
State Management Learn how GraphQL data flows into Pinia stores
Architecture Understand the overall system architecture