GraphQL Overview
Keystone generates a CRUD (create, read, update, delete) GraphQL API based on the schema definition provided in the system config.
Using the API
By default Keystone serves your GraphQL endpoint at /api/graphql
. When NODE_ENV=production
is not set, by default Keystone serves the GraphQL playground, an in-browser GraphQL IDE for debugging and exploring the API and GraphQL schema that Keystone built.
With the default configuration, the GraphQL playground and API endpoint is served on the path http://localhost:3000/api/graphql
.
The URL of the GraphQL API is often configured as an environment variable when building or developing your frontend application, when initializing your GraphQL instance (example is Apollo, but any GraphQL client is OK). For example:
const client = new ApolloClient({uri: process.env.APOLLO_CLIENT_GRAPHQL_URI || 'http://localhost:3000/api/graphql',cache: new InMemoryCache()});
If you don't like the default path you can control where the GraphQL API and playground are published by setting config.graphql.path
in the Keystone configuration. For security through obscurity, the playground and introspection is disabled when running Keystone with NODE_ENV=production
.
You can modify this behaviour using the config.graphql.playground
and config.graphql.apolloConfig
options. For example, to disable these features irrespective of the NODE_ENV
environment variables, add this to your Keystone config: graphql: { playground: false, apolloConfig: { introspection: false } }
.
Example
Consider the following system definition:
import { config, list } from '@keystone-6/core';import { text } from '@keystone-6/core/fields';export default config({lists: {User: list({ fields: { name: text() } }),},/* ... */});
This system will generate the following GraphQL API.
Note: The names and types of the generated queries and mutations are based on the names of the lists and fields in the system config.
type Query {users(where: UserWhereInput! = {}orderBy: [UserOrderByInput!]! = []take: Intskip: Int! = 0): [User!]user(where: UserWhereUniqueInput!): UserusersCount(where: UserWhereInput! = {}): Int}type User {id: ID!name: String}input UserWhereUniqueInput {id: ID}input UserWhereInput {AND: [UserWhereInput!]OR: [UserWhereInput!]NOT: [UserWhereInput!]id: IDFiltername: StringNullableFilter}input IDFilter {equals: IDin: [ID!]notIn: [ID!]lt: IDlte: IDgt: IDgte: IDnot: IDFilter}input StringNullableFilter {equals: Stringin: [String!]notIn: [String!]lt: Stringlte: Stringgt: Stringgte: Stringcontains: StringstartsWith: StringendsWith: Stringmode: QueryModenot: NestedStringNullableFilter}enum QueryMode {defaultinsensitive}input NestedStringNullableFilter {equals: Stringin: [String!]notIn: [String!]lt: Stringlte: Stringgt: Stringgte: Stringcontains: StringstartsWith: StringendsWith: Stringnot: NestedStringNullableFilter}input UserOrderByInput {id: OrderDirectionname: OrderDirection}enum OrderDirection {ascdesc}type Mutation {createUser(data: UserCreateInput!): UsercreateUsers(data: [UserCreateInput!]!): [User]updateUser(where: UserWhereUniqueInput!, data: UserUpdateInput!): UserupdateUsers(data: [UserUpdateArgs!]!): [User]deleteUser(where: UserWhereUniqueInput!): UserdeleteUsers(where: [UserWhereUniqueInput!]!): [User]}input UserUpdateInput {name: String}input UserUpdateArgs {where: UserWhereUniqueInput!data: UserUpdateInput!}input UserCreateInput {name: String}
Queries
user
type Query {user(where: UserWhereUniqueInput!): User}type User {id: ID!name: String}input UserWhereUniqueInput {id: ID}
users
type Query {users(where: UserWhereInput! = {}orderBy: [UserOrderByInput!]! = []take: Intskip: Int! = 0): [User!]}type User {id: ID!name: String}input UserWhereInput {AND: [UserWhereInput!]OR: [UserWhereInput!]NOT: [UserWhereInput!]id: IDFiltername: StringNullableFilter}input IDFilter {equals: IDin: [ID!]notIn: [ID!]lt: IDlte: IDgt: IDgte: IDnot: IDFilter}input StringNullableFilter {equals: Stringin: [String!]notIn: [String!]lt: Stringlte: Stringgt: Stringgte: Stringcontains: StringstartsWith: StringendsWith: Stringmode: QueryModenot: NestedStringNullableFilter}enum QueryMode {defaultinsensitive}input NestedStringNullableFilter {equals: Stringin: [String!]notIn: [String!]lt: Stringlte: Stringgt: Stringgte: Stringcontains: StringstartsWith: StringendsWith: Stringnot: NestedStringNullableFilter}input UserOrderByInput {id: OrderDirectionname: OrderDirection}enum OrderDirection {ascdesc}
usersCount
type Query {usersCount(where: UserWhereInput! = {}): Int}input UserWhereInput {AND: [UserWhereInput!]OR: [UserWhereInput!]NOT: [UserWhereInput!]id: IDFiltername: StringNullableFilter}input IDFilter {equals: IDin: [ID!]notIn: [ID!]lt: IDlte: IDgt: IDgte: IDnot: IDFilter}input StringNullableFilter {equals: Stringin: [String!]notIn: [String!]lt: Stringlte: Stringgt: Stringgte: Stringcontains: StringstartsWith: StringendsWith: Stringmode: QueryModenot: NestedStringNullableFilter}enum QueryMode {defaultinsensitive}input NestedStringNullableFilter {equals: Stringin: [String!]notIn: [String!]lt: Stringlte: Stringgt: Stringgte: Stringcontains: StringstartsWith: StringendsWith: Stringnot: NestedStringNullableFilter}
Mutations
createUser
type Mutation {createUser(data: UserCreateInput!): User}input UserCreateInput {name: String}type User {id: ID!name: String}
createUsers
type Mutation {createUsers(data: [UserCreateInput!]!): [User]}input UserCreateInput {name: String}type User {id: ID!name: String}
updateUser
type Mutation {updateUser(where: UserWhereUniqueInput!, data: UserUpdateInput!): User}input UserWhereUniqueInput {id: ID}input UserUpdateInput {name: String}type User {id: ID!name: String}
updateUsers
type Mutation {updateUsers(data: [UserUpdateArgs!]!): [User]}input UserUpdateArgs {where: UserWhereUniqueInput!data: UserUpdateInput!}input UserWhereUniqueInput {id: ID}input UserUpdateInput {name: String}type User {id: ID!name: String}
deleteUser
type Mutation {deleteUser(where: UserWhereUniqueInput!): User}input UserWhereUniqueInput {id: ID}type User {id: ID!name: String}
deleteUsers
type Mutation {deleteUsers(ids: [UserWhereUniqueInput!]!): [User]}input UserWhereUniqueInput {id: ID}type User {id: ID!name: String}
Errors
The Keystone GraphQL API is powered by Apollo Server. When something goes wrong with a query or mutation, one or more errors will be returned in the errors
array returned to the GraphQL client.
Keystone provides custom errors where possible, including custom error codes and messages. These error codes and messages can be used to provide useful feedback to users, and also to help identify possible bugs in your system. The following error codes can be returned from the Keystone GraphQL API.
KS_USER_INPUT_ERROR
: The input to the operation is syntactically correct GraphQL, but the values provided are invalid. E.g, anorderBy
input without any keys.KS_ACCESS_DENIED
: The operation is not allowed because either an Access Control rule prevents it, or the item does not exist.KS_FILTER_DENIED
: The filter or ordering operation is not allowed because ofisFilterable
orisOrderable
rules.KS_VALIDATION_FAILURE
: The operation is not allowed because of a validation rule.KS_LIMITS_EXCEEDED
: The user has exceeded some query limits. E.g, atake
input that is too high.KS_EXTENSION_ERROR
: An error was thrown while excuting a system extension function, such as a hook or an access control function.KS_ACCESS_RETURN_ERROR
: An invalid value was returned from an access control function.KS_RESOLVER_ERROR
: An error occured while resolving the input for a field.KS_RELATIONSHIP_ERROR
: An error occured while resolving the input relationship field.KS_PRISMA_ERROR
: An error occured while running a Prisma client operation.
A note on
KS_ACCESS_DENIED
: Returning a "not found" error from a mutation likeupdateUser({ where: { secretKey: 'abc' } })
but an "access denied" error fromupdateUser({ where: { secretKey: 'def' } })
would reveal the existence of a user with the secret key "def". To prevent leaking private information in this way, Keystone will always say "access denied" when you try to perform a mutation on an item that can't be operated on, whether that is because there is no matching record in the database, or there was but the user performing the operation doesn't have access to it.