RedwoodJS

# GraphQL

GraphQL is a fundamental part of Redwood. Having said that, you can get going without knowing anything about it, and can actually get quite far without ever having to read the docs. But to master Redwood, you'll need to have more than just a vague notion of what GraphQL is; you'll have to really grok it.

The good thing is that, besides taking care of the annoying stuff for you (namely, mapping your resolvers, which gets annoying fast if you do it yourself!), there's not many gotchas with GraphQL in Redwood. GraphQL is GraphQL. The only Redwood-specific thing you should really be aware of is resolver args.

Since there's two parts to GraphQL in Redwood, the client and the server, we've divided this doc up that way. By default, Redwood uses Apollo for both: Apollo Client for the client and Apollo Server for the server, though you can swap Apollo Client out for something else if you want. Apollo Server, not so much, but you really shouldn't have to do that unless you want to be on the bleeding edge of the GraphQL spec, in which case, why are you reading this doc anyway? Contribute a PR instead!

# Client-side

# RedwoodApolloProvider

By default, Redwood Apps come ready-to-query with the RedwoodApolloProvider. As you can tell from the name, this Provider wraps ApolloProvider. Omitting a few things, this is what you'll normally see in Redwood Apps:

// web/src/App.js

import { RedwoodApolloProvider } from '@redwoodjs/web/apollo'

// ...

const App = () => (
  <RedwoodApolloProvider>
    <Routes />
  </RedwoodApolloProvider>
)

// ...

You can use Apollo's useQuery and useMutation hooks by importing them from @redwoodjs/web, though if you're using useQuery, we recommend that you use a Cell:

// web/src/components/MutateButton.js

import { useMutation } from '@redwoodjs/web'

const MUTATION = `
  # your mutation...
`

const MutateButton = () => {
  const [mutate] = useMutation(MUTATION)

  return (
    <button onClick={() => mutate({ ... })}>
      Click to mutate
    </button>
  )
}

Note that you're free to use any of Apollo's other hooks, you'll just have to import them from @apollo/client instead. In particular, these two hooks might come in handy:

Hook Description
useLazyQuery Execute queries in response to events other than component rendering
useApolloClient Access your instance of ApolloClient

# Customizing the Apollo Client and Cache

By default, RedwoodApolloProvider configures an ApolloClient instance with 1) a default instance of InMemoryCache to cache responses from the GraphQL API and 2) an authMiddleware to sign API requests for use with Redwood's built-in auth. Beyond the cache and link params, which are used to set up that functionality, you can specify additional params to be passed to ApolloClient using the graphQLClientConfig prop. The full list of available configuration options for the client are documented here on Apollo's site.

Depending on your use case, you may want to configure InMemoryCache. For example, you may need to specify a type policy to change the key by which a model is cached or to enable pagination on a query. This article from Apollo explains in further detail why and how you might want to do this.

To configure the cache when it's created, use the cacheConfig property on graphQLClientConfig. Any value you pass is passed directly to InMemoryCache when it's created.

For example, if you have a query named search that supports Apollo's offset pagination, you could enable it by specifying:

<RedwoodApolloProvider graphQLClientConfig={{
  cacheConfig: {
    typePolicies: {
      Query: {
        fields: {
          search: {
            // Uses the offsetLimitPagination preset from "@apollo/client/utilities";
            ...offsetLimitPagination()
          }
        }
      }
    }
  }
}}>

# Swapping out the RedwoodApolloProvider

As long as you're willing to do a bit of configuring yourself, you can swap out RedwoodApolloProvider with your GraphQL Client of choice. You'll just have to get to know a bit of the make up of the RedwoodApolloProvider; it's actually composed of a few more Providers and hooks:

  • FetchConfigProvider
  • useFetchConfig
  • GraphQLHooksProvider

For an example of configuring your own GraphQL Client, see the redwoodjs-react-query-provider. If you were thinking about using react-query, you can also just go ahead and install it!

Note that if you don't import RedwoodApolloProvider, it won't be included in your bundle, dropping your bundle size quite a lot!

# Server-side

# Understanding Default Resolvers

According to the spec, for every field in your sdl, there has to be a resolver in your Services. But you'll usually see fewer resolvers in your Services than you technically should. And that's because if you don't define a resolver, Apollo Server will.

The key question Apollo Server asks is: "Does the parent argument (in Redwood apps, the parent argument is named root—see Redwood's Resolver Args) have a property with this resolver's exact name?" Most of the time, especially with Prisma Client's ergonomic returns, the answer is yes.

Let's walk through an example. Say our sdl looks like this:

// api/src/graphql/user.sdl.js

export const schema = gql`
  type User {
    id: Int!
    email: String!
    name: String
  }

  type Query {
    users: [User!]!
  }
`

So we have a User model in our schema.prisma that looks like this:

model User {
  id    Int     @id @default(autoincrement())
  email String  @unique
  name  String?
}

If you create your Services for this model using Redwood's generator (yarn rw g services user), your Services will look like this:

// api/src/services/user/user.js

import { db } from 'src/lib/db'

export const users = () => {
  return db.user.findMany()
}

Which begs the question: where are the resolvers for the User fields—id, email, and name? All we have is the resolver for the Query field, users.

As we just mentioned, Apollo defines them for you. And since the root argument for id, email, and name has a property with each resolvers' exact name (i.e. root.id, root.email, root.name), it'll return the property's value (instead of returning undefined, which is what Apollo would do if that weren't the case).

But, if you wanted to be explicit about it, this is what it would look like:

// api/src/services/user/user.js

import { db } from 'src/lib/db'

export const users = () => {
  return db.user.findMany()
}

export const Users = {
  id: (_args, { root }) => root.id,
  email: (_args, { root }) => root.email,
  name: (_args, { root }) => root.name,
}

The terminological way of saying this is, to create a resolver for a field on a type, in the Service, export an object with the same name as the type that has a property with the same name as the field.

Sometimes you want to do this since you can do things like add completely custom fields this way:

export const Users = {
  id: (_args, { root }) => root.id,
  email: (_args, { root }) => root.email,
  name: (_args, { root }) => root.name,
  age: (_args, { root }) => new Date().getFullYear() - root.birthDate.getFullYear()}

# Redwood's Resolver Args

According to the spec, resolvers take four arguments: args, obj, context, and info. In Redwood, resolvers do take these four arguments, but what they're named and how they're passed to resolvers is slightly different:

  • args is passed as the first argument
  • obj is named root (all the rest keep their names)
  • root, context, and info are wrapped into an object; this object is passed as the second argument

Here's an example to make things clear:

export const Post = {
  user: (args, { root, context, info }) => db.post.findUnique({ where: { id: root.id } }).user()
}

Of the four, you'll see args and root being used a lot.

Argument Description
args The arguments provided to the field in the GraphQL query
root The previous return in the resolver chain
context Holds important contextual information, like the currently logged in user
info Holds field-specific information relevant to the current query as well as the schema details

There's so many terms!

Half the battle here is really just coming to terms. To keep your head from spinning, keep in mind that everybody tends to rename obj to something else: Redwood calls it root, Apollo calls it parent. obj isn't exactly the most descriptive name in the world.

# Context

In Redwood, the context object that's passed to resolvers is actually available to all your Services, whether or not they're serving as resolvers. Just import it from @redwoodjs/api:

import { context } from '@redwoodjs/api

# How to Modify the Context

Because the context is read-only in your services, if you need to modify it, then you need to do so in the createGraphQLHandler.

To populate or enrich the context on a per-request basis with additional attributes, set the context attribute createGraphQLHandler to a custom ContextFunction that modifies the context.

For example, if we want to populate a new, custom ipAddress attribute on the context with the information from the request's event, declare the setIpAddress ContextFunction as seen here:

// api/src/functions/graphql.js

// ...

const ipAddress = ({ event }) => {
  return (
    event?.headers?.['client-ip'] ||
    event?.requestContext?.identity?.sourceIp ||
    'localhost'
  )
}

const setIpAddress = async ({ event, context }) => {
  context.ipAddress = ipAddress({ event })
}

export const handler = createGraphQLHandler({
  getCurrentUser,
  loggerConfig: {
    logger,
    options: { operationName: true, tracing: true },
  },
  schema: makeMergedSchema({
    schemas,
    services: makeServices({ services }),
  }),
  context: setIpAddress,
  onException: () => {
    // Disconnect from your database with an unhandled exception.
    db.$disconnect()
  },
})

Note: If you use the preview GraphQL Helix/Envelop graphql-server package and a custom ContextFunction to modify the context in the createGraphQL handler, the function is provided only the context and not event. However, the event information is available as an attribute of the context as context.event. Therefore, in the above example, one would fetch the ip address from the event this way: ipAddress({ event: context.event }).

# The Root Schema

Did you know that you can query redwood? Try it in the GraphQL Playground (you can find the GraphQL Playground at http://localhost:8911/graphql when your dev server is running—yarn rw dev api):

query {
  redwood {
    version
    currentUser
  }
}

How is this possible? Via Redwood's root schema. The root schema is where things like currentUser are defined.

Now that you've seen the sdl, be sure to check out the resolvers.

# Logging

Logging is essential in production apps to be alerted about critical errors and to be able to respond effectively to support issues. In staging and development environments, logging helps you debug queries, resolvers and cell requests.

We want to make logging simple when using RedwoodJS and therefore have configured the api-side GraphQL handler to log common information about your queries and mutations. Log statements also be optionally enriched with operation names, user agents, request ids, and performance timings to give you move visibility into your GraphQL api.

By configuring the GraphQL handler to use your api side RedwoodJS logger, any errors and other log statements about the GraphQL execution will be logged to the destination you've set up: to standard output, file, or transport stream.

You configure the logger using the loggerConfig that accepts a logger and s set of GraphQL Logger Options.

# Configure the GraphQL Logger

A typical GraphQLHandler graphql.ts is as follows:

// api/src/functions/graphql.ts
// ...

import { logger } from 'src/lib/logger'

// ...
export const handler = createGraphQLHandler({
  loggerConfig: { logger, options: {} },
// ...
})

# Log Common Information

The loggerConfig takes several options that logs meaningful information along the graphQL execution lifecycle.

Option Description
data Include response data sent to client.
operationName Include operation name. The operation name is a meaningful and explicit name for your operation. It is only required in multi-operation documents, but its use is encouraged because it is very helpful for debugging and server-side logging. When something goes wrong (you see errors either in your network logs, or in the logs of your GraphQL server) it is easier to identify a query in your codebase by name instead of trying to decipher the contents. Think of this just like a function name in your favorite programming language. See https://graphql.org/learn/queries/#operation-name
requestId Include the event's requestId, or if none, generate a uuid as an identifier.
query Include the query. This is the query or mutation (with fields) made in the request.
tracing Include the tracing and timing information. This will
userAgent Include the browser (or client's) user agent. This can be helpful to know what type of client made the request to resolve issues when encountering errors or unexpected behavior.

Therefore, if you wish to log the GraphQL query made, the data returned, and the operationName used, you would

// api/src/functions/graphql.ts

export const handler = createGraphQLHandler({
  loggerConfig: {
    logger,
    options: { data: true, operationName: true, query: true },
  },
// ...
})

# Benefits of Logging

Benefits of logging common GraphQL request information include debugging, profiling, and resolving issue reports.

# Operation Name Identifies Cells

The operation name is a meaningful and explicit name for your operation. It is only required in multi-operation documents, but its use is encouraged because it is very helpful for debugging and server-side logging.

Because your cell typically has a unique operation name, logging this can help you identify which cell made a request.

# RequestId for Support Issue Resolution

Often times, your deployment provider will provide a request identifier to help reconcile and track down problems at an infrastructure level. For example, AWS API Gateway and AWS Lambda (used by Netlify, for example) provides requestId on the event.

You can include the request identifier setting the requestId logger option to true.

// api/src/functions/graphql.ts
// ...
export const handler = createGraphQLHandler({
  loggerConfig: { logger, options: { requestId: true } },
// ...

And then, when working to resolve a support issue with your deployment provider, you can supply this request id to help them track down and investigate the problem more easily.

# No Need to Log within Services

By configuring your GraphQL logger to include data and query information about each request you can keep your service implementation clean, concise and free of repeated logger statements in every resolver -- and still log the useful debugging information.

// api/src/functions/graphql.ts
// ...
export const handler = createGraphQLHandler({
  loggerConfig: { logger, options: { data: true, operationName: true, query: true } },
// ...

// api/src/services/posts.js
//... 
export const post = async ({ id }) => {
  return await db.post.findUnique({
    where: { id },
  })
}
//... 

The GraphQL handler take care of will then take take of logging your query and data -- as long as your logger is setup to log at the info level and above. You can also disable the statements in production by just logging at the warn and above level.

api | INFO [2021-07-09 14:20:11.656 +0000] (apollo-graphql-server): GraphQL requestDidStart
api |     query: "query ($id: Int!) {\n  post(id: $id) {\n    id\n    title\n    body\n    createdAt\n    publishedAt\n    updatedAt\n    __typename\n  }\n}\n"
api | DEBUG [2021-07-09 14:20:11.657 +0000] (apollo-graphql-server): GraphQL executionDidStart
api |     operationName: null
api |     query: "query ($id: Int!) {\n  post(id: $id) {\n    id\n    title\n    body\n    createdAt\n    publishedAt\n    updatedAt\n    __typename\n  }\n}\n"
api | INFO [2021-07-09 14:20:12.114 +0000] (apollo-graphql-server): GraphQL willSendResponse
api |     data: {
api |       "post": {
api |         "id": 2,
api |         "title": "Lime Tree Arbour",
api |         "body": "The wind in the trees is whispering \\ Whispering low that I love her \\ She puts her hand over mine \\ Down in the lime tree arbour",
api |         "createdAt": "2021-03-18T05:32:39.258Z",
api |         "publishedAt": "2021-03-18T05:32:39.258Z",
api |         "updatedAt": "2021-07-09T01:52:08.005Z",
api |         "__typename": "Post"
api |       }
api |     }
api |     query: "query ($id: Int!) {\n  post(id: $id) {\n    id\n    title\n    body\n    createdAt\n    publishedAt\n    updatedAt\n    __typename\n  }\n}\n"

but keep your services concise!

# Send to Third-party Transports

Stream to third-party log and application monitoring services vital to production logging in serverless environments like logFlare, Datadog or LogDNA

# Supports Log Redaction

Everyone has heard of reports that Company X logged emails, or passwords to files or systems that may not have been secured. While RedwoodJS logging won't necessarily prevent that, it does provide you with the mechanism to ensure that won't happen.

To redact sensitive information, you can supply paths to keys that hold sensitive data using the RedwoodJS logger redact option.

Because this logger is used with the GraphQL handler, it will respect any redaction paths setup.

For example, you have chosen to log data return by each request, then you may want to redact sensitive information, like email addresses from yur logs.

Here is an example of an application /api/src/lib/logger.ts configured to redact email addresses. Take note of the path data.users[*].email as this says, in the data attribute, redact the email from every user:

// /api/src/lib/logger.ts
import { createLogger, redactionsList } from '@redwoodjs/api/logger'

export const logger = createLogger({
  options: {
    redact: [...redactionsList, 'email', 'data.users[*].email'],
  },
})

# Timing Traces and Metrics

Often you want to measure and report how long your queries take to execute and respond. You may already be measuring these durations at the database level, but you can also measure the time it takes for your the GraphQL server to parse, validate, and execute the request.

You may turn on logging these metrics via the tracing GraphQL configuration option.

// api/src/functions/graphql.ts
// ...
export const handler = createGraphQLHandler({
  loggerConfig: { logger, options: { tracing: true } },
// ...

Let's say we wanted to get some benchmark numbers for the "find post by id" resolver

  return await db.post.findUnique({
    where: { id },
  })

We see that this request took about 500 msecs (note: duration is reported in nanoseconds).

For more details about the information logged and its format, see Apollo Tracing.

pi | INFO [2021-07-09 14:25:52.452 +0000] (apollo-graphql-server): GraphQL willSendResponse
api |     tracing: {
api |       "version": 1,
api |       "startTime": "2021-07-09T14:25:51.931Z",
api |       "endTime": "2021-07-09T14:25:52.452Z",
api |       "duration": 521131526,
api |       "execution": {
api |         "resolvers": [
api |           {
api |             "path": [
api |               "post"
api |             ],
api |             "parentType": "Query",
api |             "fieldName": "post",
api |             "returnType": "Post!",
api |             "startOffset": 1787428,
api |             "duration": 519121497
api |           },
api |           {
api |             "path": [
api |               "post",
api |               "id"
api |             ],
api |             "parentType": "Post",
api |             "fieldName": "id",
api |             "returnType": "Int!",
api |             "startOffset": 520982888,
api |             "duration": 25140
api |           },
... more paths follow ...
api |         ]
api |       }
api |     }

By logging the operation name and extracting the duration for each query, you can easily collect and benchmark query performance.

# Security

We'll document more GraphQL security best practices as Redwood reaches a v1.0 release candidate. For now, know that Redwood already has some baked-in best practices; for example, when deploying GraphQL to production, GraphQL Playground is automatically disabled.

# Secure Services

Some of the biggest security improvements we'll be making revolve around Services (which are intimately linked to GraphQL since they're wrapped into your resolvers). For v1.0 we plan to make all of your GraphQL resolvers secure by default. You can even opt into this behavior now—see the Secure Services section.

# Introspection and Playground Disabled in Production

Because it is often useful to ask a GraphQL schema for information about what queries it supports, GraphQL allows us to do so using the introspection system.

The GraphQL Playground is a way for you to interact with your schema and try out queries and mutations. It can show you the schema by inspecting it. You can find the GraphQL Playground at http://localhost:8911/graphql when your dev server is running.

Because both introspection and the playground share possibly sensitive information about your data model, your data, your queries and mutations, best practices for deploying a GraphQL Server call to disable these in production, RedwoodJS only enables introspection and the playground when running in development. That is when process.env.NODE_ENV === 'development'.

# Query Depth Limit

Attackers often submit expensive, nested queries to abuse query depth that could overload your database or expend costly resources.

Typically, these types of unbounded, complex and expensive GraphQL queries are usually huge deeply nested and take advantage of an understanding of your schema (hence why schema introspection is disabled by default in production) and the data model relationships to create "cyclical" queries.

An example of a cyclical query here takes advantage of knowing that and author has posts and each post has and author ... that has posts ... that has an another that ... etc.

This cyclical query has a depth of 8.

// cyclical query example
// depth: 8+
query cyclical {
  author(id: 'jules-verne') {
    posts {
      author {
        posts {
          author {
            posts {
              author {
                ... {
                  ... # more deep nesting!
                }
              }
            }
          }
        }
      }
    }
  }
}

To mitigate the risk of attacking your application via deeply nested queries, RedwoodJS by default sets the Query Depth Limit to 11.

You can change the default value via the depthLimitOptions setting when creating your GraphQL handler.

You depthLimitOptions are maxDepth or ignore stops recursive depth checking based on a field name. Ignore can be either a string or regexp to match the name, or a function that returns a boolean.

For example:

// ...
export const handler = createGraphQLHandler({
  loggerConfig: { logger, options: { query: true } },
  depthLimitOptions: { maxDepth: 6 },
// ...
})

## FAQ

### Why Doesn't Redwood Use Something Like Nexus?

This might be one of our most frequently asked questions of all time. Here's [Tom's response in the forum](https://community.redwoodjs.com/t/anyone-playing-around-with-nexus-js/360/5): 

> We started with Nexus, but ended up pulling it out because we felt like it was too much of an abstraction over the SDL. It’s so nice being able to just read the raw SDL to see what the GraphQL API is.

<!-- TODO -->
<!-- This https://community.redwoodjs.com/t/how-to-add-resolvetype-resolver-for-interfaces/432/7 -->