How to configure GraphQL request with interceptors on the example of  JWT authentication

Tags: javascript, typescript, graphql, jwt
Published: 2021-08-25 16:29:54



1. **GraphQL request** - minimalistic and simple graphql client that can be conveniently combined with any state manager.
2. **Interceptors** - сonvenient methods for modifying requests and responses that are widely used by http clients such as axios. 
3. As part of this tutorial, we will consider a configuration option for a **GraphQL request** using the example of forwarding a header with an access token to a request and intercepting a 401 response error to refresh this token.

Link to documentation: https://www.npmjs.com/package/graphql-request 

So let's get started.


**Step 1. Installing the package**
```javascript
yarn add graphql-request graphql
```

**Step 2. Create a request context class**
```javascript
export class GQLContext {

    private client: GraphQLClient
    private snapshot: RequestSnapshot;
    private readonly requestInterceptor = new RequestStrategy();
    private readonly responseInterceptor = new ResponseStrategy();

    public req: GQLRequest;
    public res: GQLResponse;
    public isRepeated = false;

    constructor(client: GraphQLClient) {
        this.client = client
    }

    async setRequest(req: GQLRequest) {
        this.req = req
        await this.requestInterceptor.handle(this)
    }

    async setResponse(res: GQLResponse) {
        this.res = res
        await this.responseInterceptor.handle(this)
    }

    async sendRequest(): Promise<GQLResponse> {
        if (!this.snapshot) {
            this.createSnapshot()
        }
        const res = await this.client.rawRequest.apply(this.client, new NativeRequestAdapter(this)) as GQLResponse
        await this.setResponse(res)

        return this.res
    }

    async redo(): Promise<GQLResponse> {
        await this.snapshot.restore()
        this.isRepeated = true
        return await this.sendRequest()
    }


    createSnapshot() {
        this.snapshot = new RequestSnapshot(this)
    }
}
```

This class will contain data about the request, response (upon receipt), as well as store the reference to the GQL client itself.
To set the request context, two methods are used: **setRequest** and **setResponse**. Each of them applies an appropriate strategy of using interceptors, each of which we will discuss below.

Let's take a look at the **snapshot** structure:

```javascript
export class RequestSnapshot {

    instance: GQLContext;
    init: GQLRequest;

    constructor(ctx: GQLContext) {
        this.instance = ctx
        this.init = ctx.req
    }

    async restore() {
        await this.instance.setRequest(this.init)
    }
}
```

The **snapshot** receives a reference to the execution context, and also saves the state of the original request for subsequent restoration (if necessary) using the **restore** method

The **sendRequest** method will serve as a wrapper for **gql-request**, making it possible to create a snapshot of the original request using the **createSnapshot** method

**NativeRequestAdapter** is an adapter that serves to bring our context object to the form that the native **gql-request** can work with:
```javascript
export function NativeRequestAdapter (ctx: GQLContext){
    return Array.of(ctx.req.type, ctx.req.variables, ctx.req.headers)
}
```

The **redo** method is used to repeat the original request and consists of three basic steps:
1) Reconstructing the context of the original request
2) Set the flag indicating that the request is repeated
3) Repeat the original request

**Step 3. Registering our own error type**
```javascript
export class GraphQLError extends Error {
    code: number;

    constructor(message: string, code: number) {
        super(message)
        this.code = code
    }
}
```
In this case, we are simply extending the structure of a native JS error by adding a response code there.


**Step 4. Writing an abstraction for an interceptor**

For writing an abstraction of an interceptor, the **"Chain of Responsibility (СoR)"** behavioral programming pattern is perfect. This pattern allows you to sequentially transfer objects along a chain of handlers, each of which independently decides how exactly the received object should be processed (in our case, the object will be our request context), as well as whether it is worth passing it further along the chain.
So let's take a closer look at this concept:

```javascript
export type GQLRequest = {
    type: string;
    variables?: any;
    headers?: Record<string, string>
}
export type GQLResponse = {
    data: any
    extensions?: any
    headers: Headers,
    status: number
    errors?: any[];
}


interface Interceptor {
    setNext(interceptor: Interceptor): Interceptor;

    intercept(type: GQLContext): Promise<GQLContext>;
}

export abstract class AbstractInterceptor implements Interceptor {

    private nextHandler: Interceptor;

    public setNext(interceptor: Interceptor): Interceptor {
        this.nextHandler = interceptor
        return interceptor
    }

    public async intercept(ctx: GQLContext) {
        if (this.nextHandler) return await this.nextHandler.intercept(ctx)

        return ctx
    }

}
```
You can see two methods here:

1. **setNext** - designed to set the next interceptor in the chain, a reference to which we will store in the **nextHandler** property
2. **intercept** - the parent method is intended to transfer control to the next handler. This method will be used by child classes if necessary 

**Step 5. Request Interceptor Implementation**

```javascript
export class AuthInterceptor extends AbstractInterceptor{
    intercept(ctx: GQLContext): Promise<GQLContext> {
        if (typeof window !== 'undefined') {

            const token = window.localStorage.getItem('token')
            if (!!token && token !== 'undefined') {
                ctx.req.headers = {
                ...ctx.req.headers, 
                Authorization: `Bearer ${token}`
                }
            }
        }
        return super.intercept(ctx) 
    }

}
```
This interceptor gets the access token from **localStorage** and adds a header with the token to the request context

**Step 6. Response Interceptor Implementation**

Here we will implement interception of 401 errors and, if received, we will make a request to refresh the token and repeat the original request.
```javascript
export const REFRESH_TOKEN = gql`
    query refreshToken {
        refreshToken{
            access_token
        }
    }
`

export class HandleRefreshToken extends AbstractInterceptor {
    async intercept(ctx: GQLContext): Promise<GQLContext> {

        if ( !('errors' in ctx.res)) return await super.intercept(ctx)

        const exception = ctx.res.errors[0]?.extensions?.exception

        if (!exception) return await super.intercept(ctx)

        const Error = new GraphQLError(exception.message, exception.status)
        if (Error.code === 401 && !ctx.isRepeated && typeof window !== 'undefined') {
            try {
                await ctx.setRequest({type: REFRESH_TOKEN})
                const res = await ctx.sendRequest()
                localStorage.setItem('token', res.refreshToken.access_token)
                await ctx.redo()

                return await super.intercept(ctx)
            } catch (e) {
                throw Error
            }
        }
        throw Error
    }
}
```

1.  First, we check if there are any errors in the request. If not, then we transfer control to the next handler. If so, we are trying to get the exсeption.

2.  From the exсeption we get the response status and the error code

3.  Сheck if the error code is 401, then we make a request to refresh the token, and write a new access token in **localStorage**

4. Then we repeat the original request using the **redo** method, which we discussed earlier.

5. If this operation is successful, then we pass the request to the next handler. Otherwise, throw an error and stop processing.

**Step 7. Writing a strategy abstraction**

```javascript
export abstract class InterceptStrategy {

    protected makeChain(collection: AbstractInterceptor[]) {
        collection.forEach((handler, index) => collection[index + 1] && handler.setNext(collection[index + 1]))
    }

    abstract handle(ctx: GQLContext): any;
}
```

Strategy abstraction is represented by two methods:
1. **makeChain** - a helper that allows you to conveniently assemble a chain of handlers from an array
2. **handle** - a method that implements the main logic of the processing strategy, we will describe it in the implementations


**Step 8. Implementing request and response interception strategies**
```javascript
export class RequestStrategy extends InterceptStrategy{

    async handle(ctx: GQLContext): Promise<GQLContext> {
        const handlersOrder: AbstractInterceptor[] = [
            new AuthInterceptor(),
        ]
        this.makeChain(handlersOrder)

        return await handlersOrder[0].intercept(ctx)
    }
}
export class ResponseStrategy extends InterceptStrategy{

    async handle(ctx: GQLContext): Promise<GQLResponse['data']> {
        const handlersOrder: AbstractInterceptor[] = [
            new HandleRefreshToken(),
            new RetrieveDataInterceptor(),
        ]
        this.makeChain(handlersOrder)

        return await handlersOrder[0].intercept(ctx)
    }
}
```

As we can see, both strategies look absolutely identical in structure. Notice the handle method, which:

1. Determines the order of invocation of handlers
2. Creates a chain of them using the parent **makeChain** method
3. And starts the processing 

**Step 9. Putting it all together.**
```javascript
const request = async function (this: GraphQLClient, type: string, variables: any, headers = {}): Promise<any> {

    const ctx = new GQLContext(this)
    await ctx.setRequest({type, variables, headers})
    try {
        await ctx.sendRequest()
    } catch (e) {
        await ctx.setResponse(e.response)
    }

    return ctx.res
}

GraphQLClient.prototype.request = request

export const client = new GraphQLClient('http://localhost:4000/graphql', {
    credentials: 'include',
})
```

1. Override the base request method supplied by the package.
2. Inside our method, create a context
3. Set the initial parameters of the request
4. Send a request and set a response
5. Returning response data
6. Export the created client

Thanks for reading. I would be glad to receive your feedback.
Link to repository: https://github.com/IAlexanderI1994/gql-request-article