Namespacing by Separation of Concerns
Organize root-level operation fields into namespaces
Most GraphQL APIs provide their capabilities as root-level fields of the Query
and Mutation
types, resulting in a flat structure. For example, the GitHub GraphQL API has approximately 200 of these root-level fields! Even with tools like the Apollo Explorer, navigating and understanding larger "flat" graphs can be difficult.
To improve the logical organization of our graph's capabilities, we can define namespaces for our root-level operation fields. These are object types that in turn define query and mutation fields that are all related to a particular concern.
For example, we can define all the mutation fields related to User
objects in a UsersMutations
namespace object:
1type UsersMutations {
2 create(profile: UserProfileInput!): User!
3 block(id: ID!): User!
4}
We can then define a similar namespace for Comment
mutations:
1type CommentsMutations {
2 create(comment: CommentInput!): Comment!
3 delete(id: ID!): Comment!
4}
Now, both our User
and Comment
types have an associated create
mutation field, which is valid because each is defined within a separate namespace type.
Finally, we can add our namespace types as the return values for root-level fields of the Mutation
type:
1type Mutation {
2 users: UsersMutations!
3 comments: CommentsMutations!
4}
We can use the same pattern for queries that involve User
and Comment
types:
1type UsersQueries {
2 all: [User!]!
3}
4
5type CommentsQueries {
6 byUser(user: ID!): [Comment!]!
7}
8
9# Add a single root-level namespace-type which wraps other queries
10type Query {
11 users: UsersQueries!
12 comments: CommentsQueries!
13}
With our namespaces defined, client operations now use a nested format, which provides context on which type is being interacted with:
1mutation CreateNewUser($userProfile: UserProfileInput!) {
2 users {
3 create(profile: $userProfile) {
4 id
5 firstName
6 lastName
7 }
8 }
9}
10
11query FetchAllUsers {
12 users {
13 all {
14 id
15 firstName
16 lastName
17 }
18 }
19}
user
in the field names of the UsersQueries
type, because we already know all these fields apply to User
objects.Namespaces for serial mutations
Unlike all other fields in a GraphQL operation, the root-level fields of the Mutation
type must be resolved serially instead of in parallel. This can help prevent two mutation fields from interacting with the same data simultaneously, which might cause a race condition.
1mutation DoTwoThings {
2 one {
3 success
4 }
5 # The `two` field is not resolved until after `one` is resolved.
6 # It is not resolved at all if resolving `one` results in an error.
7 two {
8 success
9 }
10}
With namespaces, your mutation fields that actually modify data are no longer root-level fields (instead, your namespace objects are). Because of this, the mutation fields are resolved in parallel. In many systems, this doesn't present an issue (for example, you probably want to use another mechanism in your mutation resolvers to ensure transactional consistency, such as a saga orchestrator).
1mutation DoTwoNestedThings(
2 $createInput: CreateReviewInput!
3 $deleteInput: DeleteReviewInput!
4) {
5 reviews {
6 create(input: $createInput) {
7 success
8 }
9 # Is resolved in parallel with `create`
10 delete(input: $deleteInput) {
11 success
12 }
13 }
14}
If you want to guarantee serial execution in a particular operation, you can use client-side aliases to create two root fields that are resolved serially:
1mutation DoTwoNestedThingsInSerial(
2 $createInput: CreateReviewInput!
3 $deleteInput: DeleteReviewInput!
4) {
5 a: reviews {
6 create(input: $createInput) {
7 success
8 }
9 }
10 # Is resolved serially after `a` is resolved
11 b: reviews {
12 delete(input: $deleteInput) {
13 success
14 }
15 }
16}
Caveats
As pointed out by members from the GraphQL Technical Steering Committee, while the above approach does execute in a GraphQL server, it does not satisfy the GraphQL spec requirement that:
resolution of fields other than top-level mutation fields must always be side effect-free and idempotent.
Instead it is recommended that any GraphQL mutations be defined at the root level so they are executed serially and in accordance with the expectations of the specification.
At this time the only solution the GraphQL specification provides for grouping related mutations is field naming conventions and ordering these fields carefully - there's currently no spec-compliant solution to having an overwhelming number of fields on the root mutation type. However, there are some interesting proposals to address this issue that we encourage community members to review and provide feedback on, in particular the proposal for GraphQL Namespaces and the proposal for serial fields
.