├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .gitpod.yml ├── .graphqlconfig ├── .node-version ├── .nvmrc ├── README.md ├── app.vue ├── codegen.yml ├── gql ├── introspection.ts ├── queries │ ├── all-countries.graphql │ └── all-countries.ts ├── schema.graphql └── schema.ts ├── nuxt.config.ts ├── package.json ├── pages ├── countries.vue └── index.vue ├── plugins └── urql.ts ├── pnpm-lock.yaml └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .nuxt 3 | .output 4 | gql/**/*.ts 5 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: "vue-eslint-parser", 3 | parserOptions: { 4 | ecmaVersion: 12, 5 | parser: "@typescript-eslint/parser", 6 | sourceType: "module", 7 | }, 8 | env: { 9 | browser: true, 10 | node: true, 11 | }, 12 | plugins: [ 13 | "vue", 14 | "@typescript-eslint" 15 | ], 16 | extends: [ 17 | "eslint:recommended", 18 | "plugin:@typescript-eslint/recommended", 19 | "plugin:vue/vue3-strongly-recommended", 20 | "@vue/typescript/recommended", 21 | "@vue/eslint-config-typescript/recommended", 22 | ], 23 | rules: { 24 | "vue/multi-word-component-names": "off", 25 | "vue/singleline-html-element-content-newline": "off", 26 | "vue/max-attributes-per-line": ["error", {"singleline": 3}] 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | .output 4 | .nuxt 5 | *.log 6 | 7 | .pnp.* 8 | .yarn/* 9 | !.yarn/patches 10 | !.yarn/plugins 11 | !.yarn/releases 12 | !.yarn/sdks 13 | !.yarn/versions 14 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | # This configuration file was automatically generated by Gitpod. 2 | # Please adjust to your needs (see https://www.gitpod.io/docs/config-gitpod-file) 3 | # and commit this file to your remote git repository to share the goodness with others. 4 | 5 | tasks: 6 | - init: pnpm install 7 | command: pnpm dev 8 | 9 | 10 | -------------------------------------------------------------------------------- /.graphqlconfig: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Untitled GraphQL Schema", 3 | "schemaPath": "gql/schema.graphql", 4 | "extensions": { 5 | "endpoints": { 6 | "Default GraphQL Endpoint": { 7 | "url": "https://countries.trevorblades.com/", 8 | "headers": { 9 | "user-agent": "JS GraphQL" 10 | }, 11 | "introspect": true 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 20.10.0 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.11.1 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nuxt 3 example of graphql with ssr 2 | 3 | > :electric_plug: ***Available as a nuxt 3 module at https://github.com/gbicou/nuxt-urql*** 4 | 5 | This is a example of nuxt3 application with : 6 | 7 | * [graphql code generation](https://www.graphql-code-generator.com/) 8 | * [urql graphql client](https://formidable.com/open-source/urql/) 9 | * SSR 10 | * typescript everywhere 11 | * composition api 12 | 13 | Using countries graphql api from Trevor Blades https://countries.trevorblades.com/ 14 | 15 | ## Docs 16 | 17 | We recommend to look at the [nuxt3 documentation](http://v3.nuxtjs.org). 18 | 19 | ## Setup 20 | 21 | Make sure to install the dependencies 22 | 23 | ```bash 24 | pnpm install 25 | ``` 26 | 27 | ## Development 28 | 29 | Rebuild typescript sources (schema, introspection and operations) from graphql files 30 | 31 | ```bash 32 | pnpm graphql-codegen 33 | ``` 34 | 35 | Start the development server on http://localhost:3000 36 | 37 | ```bash 38 | pnpm dev 39 | ``` 40 | 41 | ## Production 42 | 43 | Build the application for production: 44 | 45 | ```bash 46 | pnpm build 47 | ``` 48 | 49 | Checkout the [deployment documentation](https://v3.nuxtjs.org/docs/deployment). 50 | 51 | ## Demo 52 | 53 | Application is deployed : 54 | 55 | * on vercel @ https://nuxt3-urql.vercel.app/ 56 | * on netlify @ https://nuxt3-urql.netlify.app/ 57 | 58 | 59 | -------------------------------------------------------------------------------- /app.vue: -------------------------------------------------------------------------------- 1 | 2 | Nuxt3 + urql 3 | 4 | index - 5 | countries 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /codegen.yml: -------------------------------------------------------------------------------- 1 | schema: ./gql/schema.graphql 2 | documents: ./gql/**/*.graphql 3 | #hooks: 4 | # afterOneFileWrite: 5 | # - eslint --fix 6 | config: 7 | scalars: 8 | Long: number 9 | defaultScalarType: never 10 | useTypeImports: true 11 | generates: 12 | gql/schema.ts: 13 | plugins: 14 | - typescript 15 | - typescript-urql-graphcache 16 | gql/: 17 | preset: near-operation-file 18 | presetConfig: 19 | baseTypesPath: schema.ts 20 | extension: .ts 21 | config: 22 | preResolveTypes: false 23 | plugins: 24 | - typescript-operations 25 | - typed-document-node 26 | # - typescript-vue-urql 27 | gql/introspection.ts: 28 | plugins: 29 | - urql-introspection 30 | -------------------------------------------------------------------------------- /gql/introspection.ts: -------------------------------------------------------------------------------- 1 | import type { IntrospectionQuery } from 'graphql'; 2 | export default { 3 | "__schema": { 4 | "queryType": { 5 | "name": "Query" 6 | }, 7 | "mutationType": null, 8 | "subscriptionType": null, 9 | "types": [ 10 | { 11 | "kind": "OBJECT", 12 | "name": "Continent", 13 | "fields": [ 14 | { 15 | "name": "code", 16 | "type": { 17 | "kind": "NON_NULL", 18 | "ofType": { 19 | "kind": "SCALAR", 20 | "name": "Any" 21 | } 22 | }, 23 | "args": [] 24 | }, 25 | { 26 | "name": "countries", 27 | "type": { 28 | "kind": "NON_NULL", 29 | "ofType": { 30 | "kind": "LIST", 31 | "ofType": { 32 | "kind": "NON_NULL", 33 | "ofType": { 34 | "kind": "OBJECT", 35 | "name": "Country", 36 | "ofType": null 37 | } 38 | } 39 | } 40 | }, 41 | "args": [] 42 | }, 43 | { 44 | "name": "name", 45 | "type": { 46 | "kind": "NON_NULL", 47 | "ofType": { 48 | "kind": "SCALAR", 49 | "name": "Any" 50 | } 51 | }, 52 | "args": [] 53 | } 54 | ], 55 | "interfaces": [] 56 | }, 57 | { 58 | "kind": "OBJECT", 59 | "name": "Country", 60 | "fields": [ 61 | { 62 | "name": "capital", 63 | "type": { 64 | "kind": "SCALAR", 65 | "name": "Any" 66 | }, 67 | "args": [] 68 | }, 69 | { 70 | "name": "code", 71 | "type": { 72 | "kind": "NON_NULL", 73 | "ofType": { 74 | "kind": "SCALAR", 75 | "name": "Any" 76 | } 77 | }, 78 | "args": [] 79 | }, 80 | { 81 | "name": "continent", 82 | "type": { 83 | "kind": "NON_NULL", 84 | "ofType": { 85 | "kind": "OBJECT", 86 | "name": "Continent", 87 | "ofType": null 88 | } 89 | }, 90 | "args": [] 91 | }, 92 | { 93 | "name": "currency", 94 | "type": { 95 | "kind": "SCALAR", 96 | "name": "Any" 97 | }, 98 | "args": [] 99 | }, 100 | { 101 | "name": "emoji", 102 | "type": { 103 | "kind": "NON_NULL", 104 | "ofType": { 105 | "kind": "SCALAR", 106 | "name": "Any" 107 | } 108 | }, 109 | "args": [] 110 | }, 111 | { 112 | "name": "emojiU", 113 | "type": { 114 | "kind": "NON_NULL", 115 | "ofType": { 116 | "kind": "SCALAR", 117 | "name": "Any" 118 | } 119 | }, 120 | "args": [] 121 | }, 122 | { 123 | "name": "languages", 124 | "type": { 125 | "kind": "NON_NULL", 126 | "ofType": { 127 | "kind": "LIST", 128 | "ofType": { 129 | "kind": "NON_NULL", 130 | "ofType": { 131 | "kind": "OBJECT", 132 | "name": "Language", 133 | "ofType": null 134 | } 135 | } 136 | } 137 | }, 138 | "args": [] 139 | }, 140 | { 141 | "name": "name", 142 | "type": { 143 | "kind": "NON_NULL", 144 | "ofType": { 145 | "kind": "SCALAR", 146 | "name": "Any" 147 | } 148 | }, 149 | "args": [] 150 | }, 151 | { 152 | "name": "native", 153 | "type": { 154 | "kind": "NON_NULL", 155 | "ofType": { 156 | "kind": "SCALAR", 157 | "name": "Any" 158 | } 159 | }, 160 | "args": [] 161 | }, 162 | { 163 | "name": "phone", 164 | "type": { 165 | "kind": "NON_NULL", 166 | "ofType": { 167 | "kind": "SCALAR", 168 | "name": "Any" 169 | } 170 | }, 171 | "args": [] 172 | }, 173 | { 174 | "name": "states", 175 | "type": { 176 | "kind": "NON_NULL", 177 | "ofType": { 178 | "kind": "LIST", 179 | "ofType": { 180 | "kind": "NON_NULL", 181 | "ofType": { 182 | "kind": "OBJECT", 183 | "name": "State", 184 | "ofType": null 185 | } 186 | } 187 | } 188 | }, 189 | "args": [] 190 | } 191 | ], 192 | "interfaces": [] 193 | }, 194 | { 195 | "kind": "OBJECT", 196 | "name": "Language", 197 | "fields": [ 198 | { 199 | "name": "code", 200 | "type": { 201 | "kind": "NON_NULL", 202 | "ofType": { 203 | "kind": "SCALAR", 204 | "name": "Any" 205 | } 206 | }, 207 | "args": [] 208 | }, 209 | { 210 | "name": "name", 211 | "type": { 212 | "kind": "SCALAR", 213 | "name": "Any" 214 | }, 215 | "args": [] 216 | }, 217 | { 218 | "name": "native", 219 | "type": { 220 | "kind": "SCALAR", 221 | "name": "Any" 222 | }, 223 | "args": [] 224 | }, 225 | { 226 | "name": "rtl", 227 | "type": { 228 | "kind": "NON_NULL", 229 | "ofType": { 230 | "kind": "SCALAR", 231 | "name": "Any" 232 | } 233 | }, 234 | "args": [] 235 | } 236 | ], 237 | "interfaces": [] 238 | }, 239 | { 240 | "kind": "OBJECT", 241 | "name": "Query", 242 | "fields": [ 243 | { 244 | "name": "continent", 245 | "type": { 246 | "kind": "OBJECT", 247 | "name": "Continent", 248 | "ofType": null 249 | }, 250 | "args": [ 251 | { 252 | "name": "code", 253 | "type": { 254 | "kind": "NON_NULL", 255 | "ofType": { 256 | "kind": "SCALAR", 257 | "name": "Any" 258 | } 259 | } 260 | } 261 | ] 262 | }, 263 | { 264 | "name": "continents", 265 | "type": { 266 | "kind": "NON_NULL", 267 | "ofType": { 268 | "kind": "LIST", 269 | "ofType": { 270 | "kind": "NON_NULL", 271 | "ofType": { 272 | "kind": "OBJECT", 273 | "name": "Continent", 274 | "ofType": null 275 | } 276 | } 277 | } 278 | }, 279 | "args": [ 280 | { 281 | "name": "filter", 282 | "type": { 283 | "kind": "SCALAR", 284 | "name": "Any" 285 | } 286 | } 287 | ] 288 | }, 289 | { 290 | "name": "countries", 291 | "type": { 292 | "kind": "NON_NULL", 293 | "ofType": { 294 | "kind": "LIST", 295 | "ofType": { 296 | "kind": "NON_NULL", 297 | "ofType": { 298 | "kind": "OBJECT", 299 | "name": "Country", 300 | "ofType": null 301 | } 302 | } 303 | } 304 | }, 305 | "args": [ 306 | { 307 | "name": "filter", 308 | "type": { 309 | "kind": "SCALAR", 310 | "name": "Any" 311 | } 312 | } 313 | ] 314 | }, 315 | { 316 | "name": "country", 317 | "type": { 318 | "kind": "OBJECT", 319 | "name": "Country", 320 | "ofType": null 321 | }, 322 | "args": [ 323 | { 324 | "name": "code", 325 | "type": { 326 | "kind": "NON_NULL", 327 | "ofType": { 328 | "kind": "SCALAR", 329 | "name": "Any" 330 | } 331 | } 332 | } 333 | ] 334 | }, 335 | { 336 | "name": "language", 337 | "type": { 338 | "kind": "OBJECT", 339 | "name": "Language", 340 | "ofType": null 341 | }, 342 | "args": [ 343 | { 344 | "name": "code", 345 | "type": { 346 | "kind": "NON_NULL", 347 | "ofType": { 348 | "kind": "SCALAR", 349 | "name": "Any" 350 | } 351 | } 352 | } 353 | ] 354 | }, 355 | { 356 | "name": "languages", 357 | "type": { 358 | "kind": "NON_NULL", 359 | "ofType": { 360 | "kind": "LIST", 361 | "ofType": { 362 | "kind": "NON_NULL", 363 | "ofType": { 364 | "kind": "OBJECT", 365 | "name": "Language", 366 | "ofType": null 367 | } 368 | } 369 | } 370 | }, 371 | "args": [ 372 | { 373 | "name": "filter", 374 | "type": { 375 | "kind": "SCALAR", 376 | "name": "Any" 377 | } 378 | } 379 | ] 380 | } 381 | ], 382 | "interfaces": [] 383 | }, 384 | { 385 | "kind": "OBJECT", 386 | "name": "State", 387 | "fields": [ 388 | { 389 | "name": "code", 390 | "type": { 391 | "kind": "SCALAR", 392 | "name": "Any" 393 | }, 394 | "args": [] 395 | }, 396 | { 397 | "name": "country", 398 | "type": { 399 | "kind": "NON_NULL", 400 | "ofType": { 401 | "kind": "OBJECT", 402 | "name": "Country", 403 | "ofType": null 404 | } 405 | }, 406 | "args": [] 407 | }, 408 | { 409 | "name": "name", 410 | "type": { 411 | "kind": "NON_NULL", 412 | "ofType": { 413 | "kind": "SCALAR", 414 | "name": "Any" 415 | } 416 | }, 417 | "args": [] 418 | } 419 | ], 420 | "interfaces": [] 421 | }, 422 | { 423 | "kind": "SCALAR", 424 | "name": "Any" 425 | } 426 | ], 427 | "directives": [] 428 | } 429 | } as unknown as IntrospectionQuery; -------------------------------------------------------------------------------- /gql/queries/all-countries.graphql: -------------------------------------------------------------------------------- 1 | query allCountries($filter: CountryFilterInput) { 2 | countries: countries(filter: $filter) { 3 | code 4 | name 5 | emoji 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /gql/queries/all-countries.ts: -------------------------------------------------------------------------------- 1 | import type * as Types from '../schema'; 2 | 3 | import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; 4 | export type AllCountriesQueryVariables = Types.Exact<{ 5 | filter?: Types.InputMaybe; 6 | }>; 7 | 8 | 9 | export type AllCountriesQuery = ( 10 | { __typename?: 'Query' } 11 | & { countries: Array<( 12 | { __typename?: 'Country' } 13 | & Pick 14 | )> } 15 | ); 16 | 17 | 18 | export const AllCountriesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"allCountries"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"CountryFilterInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"countries"},"name":{"kind":"Name","value":"countries"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"code"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"emoji"}}]}}]}}]} as unknown as DocumentNode; -------------------------------------------------------------------------------- /gql/schema.graphql: -------------------------------------------------------------------------------- 1 | # This file was generated based on ".graphqlconfig". Do not edit manually. 2 | 3 | schema { 4 | query: Query 5 | } 6 | 7 | type Continent { 8 | code: ID! 9 | countries: [Country!]! 10 | name: String! 11 | } 12 | 13 | type Country { 14 | capital: String 15 | code: ID! 16 | continent: Continent! 17 | currency: String 18 | emoji: String! 19 | emojiU: String! 20 | languages: [Language!]! 21 | name: String! 22 | native: String! 23 | phone: String! 24 | states: [State!]! 25 | } 26 | 27 | type Language { 28 | code: ID! 29 | name: String 30 | native: String 31 | rtl: Boolean! 32 | } 33 | 34 | type Query { 35 | continent(code: ID!): Continent 36 | continents(filter: ContinentFilterInput): [Continent!]! 37 | countries(filter: CountryFilterInput): [Country!]! 38 | country(code: ID!): Country 39 | language(code: ID!): Language 40 | languages(filter: LanguageFilterInput): [Language!]! 41 | } 42 | 43 | type State { 44 | code: String 45 | country: Country! 46 | name: String! 47 | } 48 | 49 | enum CacheControlScope { 50 | PRIVATE 51 | PUBLIC 52 | } 53 | 54 | input ContinentFilterInput { 55 | code: StringQueryOperatorInput 56 | } 57 | 58 | input CountryFilterInput { 59 | code: StringQueryOperatorInput 60 | continent: StringQueryOperatorInput 61 | currency: StringQueryOperatorInput 62 | } 63 | 64 | input LanguageFilterInput { 65 | code: StringQueryOperatorInput 66 | } 67 | 68 | input StringQueryOperatorInput { 69 | eq: String 70 | glob: String 71 | in: [String] 72 | ne: String 73 | nin: [String] 74 | regex: String 75 | } 76 | 77 | 78 | "The `Upload` scalar type represents a file upload." 79 | scalar Upload 80 | -------------------------------------------------------------------------------- /gql/schema.ts: -------------------------------------------------------------------------------- 1 | import { cacheExchange } from '@urql/exchange-graphcache'; 2 | import type { Resolver as GraphCacheResolver, UpdateResolver as GraphCacheUpdateResolver, OptimisticMutationResolver as GraphCacheOptimisticMutationResolver } from '@urql/exchange-graphcache'; 3 | 4 | export type Maybe = T | null; 5 | export type InputMaybe = Maybe; 6 | export type Exact = { [K in keyof T]: T[K] }; 7 | export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; 8 | export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; 9 | export type MakeEmpty = { [_ in K]?: never }; 10 | export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; 11 | /** All built-in and custom scalars, mapped to their actual values */ 12 | export type Scalars = { 13 | ID: { input: string; output: string; } 14 | String: { input: string; output: string; } 15 | Boolean: { input: boolean; output: boolean; } 16 | Int: { input: number; output: number; } 17 | Float: { input: number; output: number; } 18 | /** The `Upload` scalar type represents a file upload. */ 19 | Upload: { input: never; output: never; } 20 | }; 21 | 22 | export enum CacheControlScope { 23 | Private = 'PRIVATE', 24 | Public = 'PUBLIC' 25 | } 26 | 27 | export type Continent = { 28 | __typename?: 'Continent'; 29 | code: Scalars['ID']['output']; 30 | countries: Array; 31 | name: Scalars['String']['output']; 32 | }; 33 | 34 | export type ContinentFilterInput = { 35 | code?: InputMaybe; 36 | }; 37 | 38 | export type Country = { 39 | __typename?: 'Country'; 40 | capital?: Maybe; 41 | code: Scalars['ID']['output']; 42 | continent: Continent; 43 | currency?: Maybe; 44 | emoji: Scalars['String']['output']; 45 | emojiU: Scalars['String']['output']; 46 | languages: Array; 47 | name: Scalars['String']['output']; 48 | native: Scalars['String']['output']; 49 | phone: Scalars['String']['output']; 50 | states: Array; 51 | }; 52 | 53 | export type CountryFilterInput = { 54 | code?: InputMaybe; 55 | continent?: InputMaybe; 56 | currency?: InputMaybe; 57 | }; 58 | 59 | export type Language = { 60 | __typename?: 'Language'; 61 | code: Scalars['ID']['output']; 62 | name?: Maybe; 63 | native?: Maybe; 64 | rtl: Scalars['Boolean']['output']; 65 | }; 66 | 67 | export type LanguageFilterInput = { 68 | code?: InputMaybe; 69 | }; 70 | 71 | export type Query = { 72 | __typename?: 'Query'; 73 | continent?: Maybe; 74 | continents: Array; 75 | countries: Array; 76 | country?: Maybe; 77 | language?: Maybe; 78 | languages: Array; 79 | }; 80 | 81 | 82 | export type QueryContinentArgs = { 83 | code: Scalars['ID']['input']; 84 | }; 85 | 86 | 87 | export type QueryContinentsArgs = { 88 | filter?: InputMaybe; 89 | }; 90 | 91 | 92 | export type QueryCountriesArgs = { 93 | filter?: InputMaybe; 94 | }; 95 | 96 | 97 | export type QueryCountryArgs = { 98 | code: Scalars['ID']['input']; 99 | }; 100 | 101 | 102 | export type QueryLanguageArgs = { 103 | code: Scalars['ID']['input']; 104 | }; 105 | 106 | 107 | export type QueryLanguagesArgs = { 108 | filter?: InputMaybe; 109 | }; 110 | 111 | export type State = { 112 | __typename?: 'State'; 113 | code?: Maybe; 114 | country: Country; 115 | name: Scalars['String']['output']; 116 | }; 117 | 118 | export type StringQueryOperatorInput = { 119 | eq?: InputMaybe; 120 | glob?: InputMaybe; 121 | in?: InputMaybe>>; 122 | ne?: InputMaybe; 123 | nin?: InputMaybe>>; 124 | regex?: InputMaybe; 125 | }; 126 | 127 | export type WithTypename = Partial & { __typename: NonNullable }; 128 | 129 | export type GraphCacheKeysConfig = { 130 | Continent?: (data: WithTypename) => null | string, 131 | Country?: (data: WithTypename) => null | string, 132 | Language?: (data: WithTypename) => null | string, 133 | State?: (data: WithTypename) => null | string 134 | } 135 | 136 | export type GraphCacheResolvers = { 137 | Query?: { 138 | continent?: GraphCacheResolver, QueryContinentArgs, WithTypename | string>, 139 | continents?: GraphCacheResolver, QueryContinentsArgs, Array | string>>, 140 | countries?: GraphCacheResolver, QueryCountriesArgs, Array | string>>, 141 | country?: GraphCacheResolver, QueryCountryArgs, WithTypename | string>, 142 | language?: GraphCacheResolver, QueryLanguageArgs, WithTypename | string>, 143 | languages?: GraphCacheResolver, QueryLanguagesArgs, Array | string>> 144 | }, 145 | Continent?: { 146 | code?: GraphCacheResolver, Record, Scalars['ID'] | string>, 147 | countries?: GraphCacheResolver, Record, Array | string>>, 148 | name?: GraphCacheResolver, Record, Scalars['String'] | string> 149 | }, 150 | Country?: { 151 | capital?: GraphCacheResolver, Record, Scalars['String'] | string>, 152 | code?: GraphCacheResolver, Record, Scalars['ID'] | string>, 153 | continent?: GraphCacheResolver, Record, WithTypename | string>, 154 | currency?: GraphCacheResolver, Record, Scalars['String'] | string>, 155 | emoji?: GraphCacheResolver, Record, Scalars['String'] | string>, 156 | emojiU?: GraphCacheResolver, Record, Scalars['String'] | string>, 157 | languages?: GraphCacheResolver, Record, Array | string>>, 158 | name?: GraphCacheResolver, Record, Scalars['String'] | string>, 159 | native?: GraphCacheResolver, Record, Scalars['String'] | string>, 160 | phone?: GraphCacheResolver, Record, Scalars['String'] | string>, 161 | states?: GraphCacheResolver, Record, Array | string>> 162 | }, 163 | Language?: { 164 | code?: GraphCacheResolver, Record, Scalars['ID'] | string>, 165 | name?: GraphCacheResolver, Record, Scalars['String'] | string>, 166 | native?: GraphCacheResolver, Record, Scalars['String'] | string>, 167 | rtl?: GraphCacheResolver, Record, Scalars['Boolean'] | string> 168 | }, 169 | State?: { 170 | code?: GraphCacheResolver, Record, Scalars['String'] | string>, 171 | country?: GraphCacheResolver, Record, WithTypename | string>, 172 | name?: GraphCacheResolver, Record, Scalars['String'] | string> 173 | } 174 | }; 175 | 176 | export type GraphCacheOptimisticUpdaters = {}; 177 | 178 | export type GraphCacheUpdaters = { 179 | Query?: { 180 | continent?: GraphCacheUpdateResolver<{ continent: Maybe> }, QueryContinentArgs>, 181 | continents?: GraphCacheUpdateResolver<{ continents: Array> }, QueryContinentsArgs>, 182 | countries?: GraphCacheUpdateResolver<{ countries: Array> }, QueryCountriesArgs>, 183 | country?: GraphCacheUpdateResolver<{ country: Maybe> }, QueryCountryArgs>, 184 | language?: GraphCacheUpdateResolver<{ language: Maybe> }, QueryLanguageArgs>, 185 | languages?: GraphCacheUpdateResolver<{ languages: Array> }, QueryLanguagesArgs> 186 | }, 187 | Mutation?: {}, 188 | Subscription?: {}, 189 | Continent?: { 190 | code?: GraphCacheUpdateResolver>, Record>, 191 | countries?: GraphCacheUpdateResolver>, Record>, 192 | name?: GraphCacheUpdateResolver>, Record> 193 | }, 194 | Country?: { 195 | capital?: GraphCacheUpdateResolver>, Record>, 196 | code?: GraphCacheUpdateResolver>, Record>, 197 | continent?: GraphCacheUpdateResolver>, Record>, 198 | currency?: GraphCacheUpdateResolver>, Record>, 199 | emoji?: GraphCacheUpdateResolver>, Record>, 200 | emojiU?: GraphCacheUpdateResolver>, Record>, 201 | languages?: GraphCacheUpdateResolver>, Record>, 202 | name?: GraphCacheUpdateResolver>, Record>, 203 | native?: GraphCacheUpdateResolver>, Record>, 204 | phone?: GraphCacheUpdateResolver>, Record>, 205 | states?: GraphCacheUpdateResolver>, Record> 206 | }, 207 | Language?: { 208 | code?: GraphCacheUpdateResolver>, Record>, 209 | name?: GraphCacheUpdateResolver>, Record>, 210 | native?: GraphCacheUpdateResolver>, Record>, 211 | rtl?: GraphCacheUpdateResolver>, Record> 212 | }, 213 | State?: { 214 | code?: GraphCacheUpdateResolver>, Record>, 215 | country?: GraphCacheUpdateResolver>, Record>, 216 | name?: GraphCacheUpdateResolver>, Record> 217 | }, 218 | }; 219 | 220 | export type GraphCacheConfig = Parameters[0] & { 221 | updates?: GraphCacheUpdaters, 222 | keys?: GraphCacheKeysConfig, 223 | optimistic?: GraphCacheOptimisticUpdaters, 224 | resolvers?: GraphCacheResolvers, 225 | }; -------------------------------------------------------------------------------- /nuxt.config.ts: -------------------------------------------------------------------------------- 1 | import { defineNuxtConfig } from "nuxt/config"; 2 | 3 | export default defineNuxtConfig({ 4 | css: ["water.css/out/light.css"], 5 | build: { 6 | transpile: ["@urql/vue"] 7 | }, 8 | telemetry: false 9 | }); 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "nuxt3-urql", 4 | "version": "0.0.1", 5 | "repository": { 6 | "type": "git", 7 | "url": "git://github.com/gbicou/nuxt3-urql.git" 8 | }, 9 | "author": "Benjamin VIELLARD", 10 | "license": "MIT", 11 | "bugs": { 12 | "url": "https://github.com/gbicou/nuxt3-urql/issues" 13 | }, 14 | "homepage": "https://github.com/gbicou/nuxt3-urql#readme", 15 | "scripts": { 16 | "dev": "nuxt dev", 17 | "build": "nuxt build", 18 | "preview": "nuxt preview", 19 | "postinstall": "nuxt prepare", 20 | "lint": "eslint . --ext .js,.jsx,.ts,.tsx,.vue" 21 | }, 22 | "devDependencies": { 23 | "@graphql-codegen/cli": "^5.0.2", 24 | "@graphql-codegen/fragment-matcher": "^5.0.2", 25 | "@graphql-codegen/near-operation-file-preset": "^3.0.0", 26 | "@graphql-codegen/typed-document-node": "^5.0.9", 27 | "@graphql-codegen/typescript": "^4.0.9", 28 | "@graphql-codegen/typescript-apollo-client-helpers": "^3.0.0", 29 | "@graphql-codegen/typescript-operations": "^4.2.3", 30 | "@graphql-codegen/typescript-urql": "^4.0.0", 31 | "@graphql-codegen/typescript-urql-graphcache": "^3.1.0", 32 | "@graphql-codegen/typescript-vue-urql": "^3.2.0", 33 | "@graphql-codegen/urql-introspection": "^3.0.0", 34 | "@graphql-typed-document-node/core": "^3.2.0", 35 | "@types/node": "^20.16.10", 36 | "@typescript-eslint/eslint-plugin": "^7.18.0", 37 | "@typescript-eslint/parser": "^7.18.0", 38 | "@urql/core": "^4.3.0", 39 | "@urql/exchange-graphcache": "^6.5.1", 40 | "@urql/vue": "^1.4.1", 41 | "@vue/eslint-config-typescript": "^13.0.0", 42 | "eslint": "^8.57.1", 43 | "eslint-plugin-vue": "^9.28.0", 44 | "graphql-tag": "^2.12.6", 45 | "nuxt": "^3.16.0", 46 | "ts-node": "^10.9.2", 47 | "typescript": "~5.4.5", 48 | "water.css": "^2.1.1" 49 | }, 50 | "dependencies": { 51 | "@unhead/vue": "^1.11.7", 52 | "graphql": "^16.9.0", 53 | "vue": "^3.5.10", 54 | "vue-router": "^4.4.5" 55 | }, 56 | "packageManager": "pnpm@9.11.0" 57 | } 58 | -------------------------------------------------------------------------------- /pages/countries.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | fetching... 6 | 7 | 8 | 9 | 10 | 11 | Flag 12 | Name 13 | 14 | 15 | 16 | 17 | {{ c.emoji }} 18 | {{ c.name }} 19 | 20 | 21 | 22 | no results 23 | 24 | 25 | 26 | 27 | 57 | -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | Demo of nuxt3 SSR querying a graphql api 4 | 5 | Source code on github 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /plugins/urql.ts: -------------------------------------------------------------------------------- 1 | import { createClient, ssrExchange, fetchExchange, Client, type SSRData } from '@urql/core'; 2 | import { cacheExchange as graphCacheExchange } from '@urql/exchange-graphcache' 3 | import { defineNuxtPlugin } from '#app' 4 | import schema from '../gql/introspection'; 5 | import type { GraphCacheConfig } from '~/gql/schema'; 6 | import { ref } from "vue"; 7 | 8 | const ssrKey = '__URQL_DATA__' 9 | 10 | export default defineNuxtPlugin(nuxt => { 11 | const { vueApp } = nuxt 12 | 13 | const ssr = ssrExchange({ 14 | isClient: process.client 15 | }) 16 | 17 | // when app is created in browser, restore SSR state from nuxt payload 18 | if (process.client) { 19 | nuxt.hook('app:created', () => { 20 | ssr.restoreData(nuxt.payload[ssrKey] as SSRData) 21 | }) 22 | } 23 | 24 | // when app has rendered in server, send SSR state to client 25 | if (process.server) { 26 | nuxt.hook('app:rendered', () => { 27 | nuxt.payload[ssrKey] = ssr.extractData() 28 | }) 29 | } 30 | 31 | // use urql graphcache 32 | const cacheConfig: GraphCacheConfig = { 33 | schema, 34 | keys: { 35 | Country: (data) => data.code || null 36 | }, 37 | resolvers: { 38 | Query: { 39 | country: (_, args) => ({__typename: "Country", code: args.code}) 40 | } 41 | } 42 | // storage: process.client ? makeDefaultStorage() : undefined 43 | } 44 | const cache = graphCacheExchange(cacheConfig) 45 | 46 | const client = createClient({ 47 | url: 'https://countries.trevorblades.com/', 48 | exchanges: [ 49 | cache, 50 | ssr, // Add `ssr` in front of the `fetchExchange` 51 | fetchExchange, 52 | ] 53 | }) 54 | 55 | nuxt.provide('urql', client) 56 | vueApp.provide('$urql', ref(client)) 57 | 58 | }) 59 | 60 | declare module '#app' { 61 | interface NuxtApp { 62 | $urql: Client 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.nuxt/tsconfig.json" 3 | } 4 | --------------------------------------------------------------------------------
Demo of nuxt3 SSR querying a graphql api
5 | Source code on github 6 |