nuxt-graphql-middleware

nuxt-graphql-middleware

Expose GraphQL queries and mutations as server API routes.

Nuxt GraphQL Middleware

GraphQL in the backend, fetch in the frontend. With TypeScript support.

Idea

When using GraphQL you have to bundle your queries in your frontend build and send them with every request. If you have lots of queries and/or fragments, this can increase your frontend bundle size significantly. In addition you have to expose your entire GraphQL endpoint to the public (if you don't use persisted queries).

This module aims to fix this by performing any GraphQL requests only on the server side. It passes the response to the frontend via a simple JSON endpoint. So you can have all the benefits of GraphQL but without any bloat.

It optionally generates TypeScript type files of your schema, queries and mutations via graphql-codegen.

Features

  • GraphQL queries and mutations using graphql-request
  • Client plugin to perform queries or mutations
  • Fully flexible: Modify request headers, responses or handle errors
  • HMR for queries and mutations
  • TypeScript integration for schema, queries and mutations

Setup

Install

npm install --save nuxt-graphql-middleware

Minimal configuration needed:

module.exports = {  modules: ['nuxt-graphql-middleware'],  graphqlMiddleware: {    graphqlServer: 'http://example.com/graphql',    typescript: {      enabled: true    },    queries: {      articles: '~/pages/query.articles.graphql',    },    plugin: {      enabled: true    }  }}

Usage

With provided plugin

Simple query

asyncData({ app }) {  return app.$graphql.query('articles').then(data => {    return { articles: data.articles }  })}

With variables

Anything you provide in the second argument will be passed 1:1 as variables to the GraphQL request.

asyncData({ app }) {  return app.$graphql.query('articles', { limit: 10 }).then(data => {    return { articles: data.articles }  })}

Simple mutation

Anything you provide in the second argument is used as the mutation input.

createPost(post) {  return app.$graphql.mutate('createPost', post).then(response => {    if (response.hasError) {      this.errors.push(response.error)    }  })}

Custom requests

You can do your own requests without using the plugin. Query variables are passed as a JSON encoded string.

fetch('/__api/query?name=articles')fetch('/__api/query?name=articles&variables={"limit":10}')fetch('/__api/mutate?name=createPost', {  method: 'POST',  body: JSON.stringify(post)})

Configuration

Options

graphqlServer: string

URL of your GraphQL server.

endpointNamespace: string

Namespace where the server middleware is running, e.g. '/__api'. => http://localhost:3000/__api/query

debug: boolean

Output additional info about available queries and mutations to the console.

queries: Record<string, string>

Map of query name => filePath.

mutations: Record<string, string>

Map of mutation name => filePath.

outputPath: string

If set, the module will write the compiled queries and mutations in this folder.

plugin.enabled: boolean

Enable the helper plugin.

plugin.cacheInBrowser: boolean

Cache requests in the plugin (on client side / browser).

This enables a simple cache (using a Map) in the browser, which will cache up to 30 queries. This is useful to provide near instant rendering when going back and forth in the browser history.

Queries are cached based on their full URL (incl. query string).

plugin.cacheInServer: boolean

Same as cacheInBrowser, but the queries are also cached server side. Note: There is no way to purge this cache! Only use this if you're fine with returning potentially outdated responses.

server.middleware: (req: Request, res: Response, next: NextFunction) => any

An express middleware. Can be used for example to add an authentication or CORS check.

function(req, res, next) {  if (isLoggedIn(req.headers.cookie)) {    return next()  }  res.status(403).send()}

server.fetchOptions: Record<string, any>

Object of options passed to the fetch request to GraphQL.

server.buildHeaders: (req: Request, name: string, type: string) => Record<string, any>

Called before every request

function (req, name, type) {  if (isLoggedIn(req.headers.cookie)) {    if (type === 'mutation') {      return {        Authorization: 'Basic ' + process.env.BASIC_AUTH_WRITE      }    }  }}

server.buildEndpoint: (req: Request) => string

Called before every request. This allows you to set the URL for the GraphQL server.

This is useful if you have multiple endpoints, for example with a language prefix.

function (req) {  const language = getLanguageFromHeaders(req.headers)  return `https://example.com/${language}/graphql`}

server.onQueryResponse: (response: GraphQLResponse, req: Request, res: Response) => any

Handle GraphQL server query responses before they are sent to the client.

function(response, req, res) {  return res.json({    data: response.data,    time: Date.now()  })}

server.onQueryError: (error: ClientError, req: Request, res: Response) => any

Handle GraphQL server query errors before they are sent to the client.

server.onMutationResponse: (response: GraphQLResponse, req: Request, res: Response) => any

Handle GraphQL server mutation responses before they are sent to the client.

server.onMutationError: (error: ClientError, req: Request, res: Response) => any

Handle GraphQL server mutation errors before they are sent to the client.

typescript.enabled: boolean

Enable TypeScript integration.

typescript.schemaOutputPath: string

Folder where the downloaded schema.graphql file is saved.

typescript.skipSchemaDownload: boolean

Don't download the schema. Use this for example if you commit the schema in your repository, so that it's available during deployment.

typescript.schemaOptions:
UrlSchemaOptions

Options passed to graphql-codegen.

typescript.typesOutputPath: string

Folder where the generated graphql-schema.d.ts and graphql-operations.d.ts files are saved.

Extend $graphql plugin

If you want to add custom headers to the request made by $graphql to the middleware, create a plugin and add a beforeRequest method:

export default (pluginContext) => {  pluginContext.$graphql.beforeRequest((ctx, options) => {    options.headers['accept-language'] = ctx.route.params.lang    return options  })}

You have access to the context via the first parameter. The second parameter provides the fetch options, which you have to return.

It's also possible to return a Promise, useful if you need to handle things like a token refresh. Be aware that this method is called before every query or mutation request, so make sure it doesn't take too much time.

Integrate with nuxt-auth

Add a beforeRequest method in a custom plugin:

export default (pluginContext) => {  pluginContext.$graphql.beforeRequest((ctx, options) => {    if (ctx.$auth.loggedIn) {      options.headers['authorization'] = ctx.$auth.strategy.token.get()    }    return options  })}

Add a server.buildHeaders method, where you get the authorization header from the client request and pass it on to the server request.

buildHeaders(req, name, type) {  const auth = req.headers.authorization  if (auth) {    return {      Authorization: auth,    }  }  return {}}

Full working example

module.exports = {  modules: ['nuxt-graphql-middleware'],  graphqlMiddleware: {    graphqlServer: 'http://example.com/graphql'    endpointNamespace: '/__api'    debug: true    queries: {      route: '~/pages/query.route.graphql',      articles: '~/pages/articles/query.articles.graphql',      footer: '~/components/Footer/query.footer.graphql',    },    mutations: {      createPost: '~/components/Comment/mutation.createPost.graphql'    },    outputPath: '~/graphql_tmp'    plugin: {      enabled: true,      cacheInBrowser: true,      cacheInServer: false,    },    typescript: {      enabled: true,      schemaOutputPath: '~/schema',      typesOutputPath: '~/types',      schemaOptions: {        headers: {          Authorization: 'Basic ' + process.env.BASIC_AUTH        }      }    },    server: {      middleware: function(req, res, next) {        if (isLoggedIn(req.headers.cookie)) {          return next()        }        res.status(403).send()      },      fetchOptions: {        headers: {          Authorization: 'Basic ' + process.env.BASIC_AUTH        }      },      buildHeaders: function (req, name, type) {        if (isLoggedIn(req.headers.cookie)) {          if (type === 'mutation') {            return {              Authorization: 'Basic ' + process.env.BASIC_AUTH_WRITE            }          }        }      },      onQueryResponse: function(response, req, res) {        return res.json({          data: response.data,          time: Date.now()        })      },      onQueryError: function(error, req, res) {        return res.status(500).send()      },      onMutationResponse: function(response, req, res) {        return res.json({          data: response.data,          time: Date.now()        })      }      onMutationError: function(error, req, res) {        return res.status(500).send()      }    }  }}

TODO

  • Pass port to client plugin