Implement field permissions with FieldMiddleware
Tags: typescript, nestjs, graphql, node
Published: 2023-01-21 20:54:17

---
title: Implement field permissions with FieldMiddleware
published: true
description: Implement field permissions in NestJS
tags: typescript, nestjs, graphql, node
canonical_url: ""
# cover_image: https://direct_url_to_image.jpg
# Use a ratio of 100:42 for best results.
# published_at: 2023-01-21 19:49 +0000
---

## Introduction

You may be implementing an API in GraphQL and want to apply access control to a field of a certain type. For example, logged-in users can access a field, or only users with certain privileges can access a field, and so on.

In NestJS, such fields can be controlled using FieldMiddleware. This article explains how to create a NestJS project and apply FieldMiddleware.

The code created in this article can be found in the following repository.

[https://github.com/choco14t/field-middleware-example](https://github.com/choco14t/field-middleware-example)

## About FieldMiddleware

FieldMiddleware, as the name suggests, provides functions for adding processing before and after fields are resolved.

FieldMiddleware is defined by a function that takes two arguments of type `MiddlewareContext` and `NextFn`. The `MiddlewareContext` type is defined by Nest, but the value passed is the same as the value received by the GraphQL Resolver (`{ source, args, context, info }`). See [official GraphQL documentation](https://graphql.org/learn/execution/#root-fields-resolvers) for more details on values.

Note that FieldMiddleware does not have access to the DI container. If you want to execute an external API or access a DB, you need to make it accessible from `MiddlewareContext`.

## Example implementation with context

The following is an example of implementation.

* Define a `Query.user` that can retrieve a single user from an id.
* The user has the fields id, name, and email.
* Only users with administrative privileges can access email. If any other user tries to retrieve it, null is returned.
    
In this case, we will create a project using the following.

* `@nestjs/cli` 8.2.6
* yarn 1.22.19
* Node.js 16.15.0

```bash
nest new field-middleware-example
```

Also use Express and Apollo, following [Quick start](https://docs.nestjs.com/graphql/quick-start).

```bash
yarn add @nestjs/graphql @nestjs/apollo graphql apollo-server-express
```

### Importing a GraphQLModule

First, import the `GraphQLModule` into the `AppModule`.

```typescript
import { join } from 'path';
import { ApolloDriverConfig, ApolloDriver } from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
      sortSchema: true,
    }),
  ],
})
export class AppModule {}
```

### Create UserModule

If you try to launch the application at this point, you will get an error message that no Query or Mutation has been defined. First, we will create a `UserModule` so that we can start the application.

```bash
nest generate module user
# output
CREATE src/user/user.module.ts (81 bytes)
UPDATE src/app.module.ts (478 bytes)
```

Next, define the Type. In this case, we define an `email` with limited access and a `name` that is always accessible. At this point, both `email` and `name` are always accessible.

```typescript
import { Field, ID, ObjectType } from '@nestjs/graphql';

@ObjectType()
export class User {
  @Field(() => ID)
  id: string;

  @Field()
  name: string;

  @Field({ nullable: true })
  email: string;
}
```

Next, create a Resolver.

```bash
nest generate resolver user
# output
CREATE src/user/user.resolver.spec.ts (456 bytes)
CREATE src/user/user.resolver.ts (86 bytes)
UPDATE src/user/user.module.ts (158 bytes)
```

In the article, we only need to check the value, so we define a `Query.user` that returns a fixed value.

```typescript
import { Query, Resolver } from '@nestjs/graphql';
import { User } from './user.type';

@Resolver(() => User)
export class UserResolver {
  @Query(() => User)
  user() {
    return {
      id: '1',
      name: 'user 1',
      email: 'user_1@example.com',
    };
  }
}
```

Now the application is successfully started and the query can be executed.

### Setting up the context

Next, we set up a context to hold the logged-in user. context can be defined in a GraphQLModule.

As a side note, if the context is undefined, the context will be set to an object predefined in the Apollo package you are using.

NestJS defaults to one of the following:

| framework | context |
| --- | --- |
| Express | { req: Express.Request, res: Express.Response } |
| Fastify | { request: FastifyRequest, reply: FastifyReply } |

This time, we will check the Authorization header of the request to determine the user accessing it. Type and dummy data should be written directly in the `AppModule` at first.

```diff
 import { join } from 'path';
 import { ApolloDriverConfig, ApolloDriver } from '@nestjs/apollo';
 import { Module } from '@nestjs/common';
 import { GraphQLModule } from '@nestjs/graphql';

+ enum ViewerRole {
+   MEMBER,
+   ADMIN,
+ }
+
+ interface Viewer {
+   userName: string;
+   role: ViewerRole;
+ }
+
+ interface AppContext {
+   viewer: Viewer | undefined;
+ }
+
+ const users: Viewer[] = [
+   { userName: 'user_1', role: ViewerRole.MEMBER },
+   { userName: 'user_2', role: ViewerRole.ADMIN },
+ ];

 @Module({
   imports: [
     GraphQLModule.forRoot<ApolloDriverConfig>({
       driver: ApolloDriver,
       autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
       sortSchema: true,
+      context: ({ req }): AppContext => {
+        const token = req.headers.authorization || '';
+        const viewer = users.find((user) => user.userName === token);
+
+        return { viewer };
+      },
     }),
     UserModule,
   ],
 })
 export class AppModule {}
```

### FieldMiddleware implementation

Implement `hasAdminRole` to determine if the user has administrative privileges.

FieldMiddleware is a Generics of `FieldMiddleware<TSource, TContext, TArgs, TOutput>`, and each argument corresponds to the following type of information.

| Arguments | Corresponding type information |
| --- | --- |
| TSource | ObjectType of the passed field. |
| TContext | The context defined by the GraphQL server, which in this article is `AppContext`. |
| TArgs | Arguments used for the field. If there are no arguments, they are `{}`. |
| TOutput | The value returned by the FieldMiddleware. |

In light of the above arguments, the implementation is as follows.

```typescript
import { FieldMiddleware } from '@nestjs/graphql';
import { AppContext, ViewerRole } from '... /... /app.module';
import { User } from '... /user.type';

export const hasAdminRole: FieldMiddleware<User, AppContext> = async (
  ctx,
  next,
) => {
  const {
    context: { viewer }
  } = ctx;

  return viewer?.role === ViewerRole.ADMIN ? next() : null;
};
```

There is no reference to `TSource` in `hasAdminRole`, but it is explicit because it is Middleware used for `User`.

### Applying FieldMiddleware

Apply the FieldMiddleware implemented in the previous section to [`User.email`](http://User.email). This works by passing an array of FieldMiddleware to the `middleware` property of `FieldOptions`.

```diff
 import { Field, ID, ObjectType } from '@nestjs/graphql';
 import { hasAdminRole } from './field-middlewares/check-admin-role.middleware';

 @ObjectType()
 export class User {
   @Field(() => ID)
   id: string;

   @Field()
   name: string;

+  @Field({ nullable: true, middleware: [hasAdminRole] })
   email: string;
 }
```

### Execute Query

Finally, check that the FieldMiddleware works.

```sh
## Request by a user with non-administrative privileges
curl -H 'Content-Type: application/json' -H 'Authorization: user_1' -d '{ "query": "query { user { email } }" }' http://localhost:3000/graphql
# output
{"data":{"user":{"email":null}}

# Request as user with admin rights
curl -H 'Content-Type: application/json' -H 'Authorization: user_2' -d '{ "query": "query { user { email } }" }' http://localhost:3000/graphql
# output
{"data":{"user":{"email": "user_1@example.com"}}}
```

## Conclusion

The FieldMiddleware can be applied globally to the entire application, so if you are interested, please give it a try.

NestJS provides useful features for Code First implementations. However, compared to [graphql-shield](https://www.the-guild.dev/graphql/shield), FieldMiddleware does not provide the ability to determine if one of the rules such as `or` is satisfied. Therefore, if you want to apply complex rules, FieldMiddleware may not be able to do so.

It is possible to use graphql-shield on NestJS, so it would be better to use different libraries depending on your project.

## References

* [Field middleware | NestJS](https://docs.nestjs.com/graphql/field-middleware)
* [Extensions | NestJS](https://docs.nestjs.com/graphql/extensions)
* [packages/apollo-server-express/src/ApolloServer.ts#L48-L51](https://github.com/apollographql/apollo-server/blob/apollo-server-express%403.11.1/packages/apollo-server-express/src/ApolloServer.ts#L48-L51)
* [packages/apollo-server-fastify/src/ApolloServer.ts#L20-L23](https://github.com/apollographql/apollo-server/blob/apollo-server-fastify%403.11.1/packages/apollo-server-fastify/src/ApolloServer.ts#L20-L23)