DEV Community

Dharmen Shah
Dharmen Shah

Posted on

Github Graphql API, Apollo Angular, Nest and Deployment with DO

So after setting up nx workspace, I continued to explore Github GraphQL APIs and used Apollo Angular to consume the same. Let's see how I did that!


TL;DR

This is going to be a long one, so if you're interested in code only...

GitHub logo shhdharmen / gitalytics

πŸ™ 🐱 πŸ“Š Gitalytics - A simple overview of your Github activities

Table of Contents

1. Github GraphQL

The GitHub GraphQL API offers flexibility and the ability to define precisely the data you want to fetch.

It has only single endpoint, i.e. https://api.github.com/graphql. The endpoint remains constant no matter what operation you perform.

You can run queries on real GitHub data using the GraphQL Explorer, an integrated development environment in your browser that includes docs, syntax highlighting, and validation errors.

We are also going to need a Personal Access Token to access those APIs.

Mainly, I am going to use only user query for all operations.

2. API with NestJS

We created our NX workspace with angular-nest preset, so thanks to them, we already have a nestjs project configured in apps/api.

We are going to create only single api /github, which will work as redirection to https://api.github.com/graphql.

We could also use https://api.github.com/graphql directly in client project. But, to consume Github APIs, we need to access it with a PAT (i.e. Personal Access Token). Now, if we directly use at client side, it may expose your PAT.

2.1 Configuration

In Node.js applications, it's common to use .env files, holding key-value pairs where each key represents a particular value, to represent each environment. Running an app in different environments is then just a matter of swapping in the correct .env file.

We are going to follow nestjs configuration guidelines.

2.1.1 Environment Variables

Create a .env file ar root:

PAT=PERSON_ACCESS_TOKEN
API_URL=https://api.github.com/graphql
PORT=3000
WHITELIST_URL=http://localhost:4200
NODE_ENV=development

Enter fullscreen mode Exit fullscreen mode
Variable Description
PAT GitHub Person Access Token
API_URL GitHub GraphQL API Endpoint
PORT Where you want to run your nestjs app
WHITELIST_URL Comma separated URLs to allow access to APIs
NODE_ENV Environment where app is running

2.1.2 Variable Validation

We are also going to validate those variables, to avoid any run-time issues.

Create a file at: apps/api/src/app/config/validation.ts:

import * as Joi from '@hapi/joi';

export const validationSchema = Joi.object({
  PORT: Joi.number().required(),
  PAT: Joi.string().required(),
  API_URL: Joi.string().required(),
  WHITELIST_URL: Joi.string().required(),
  NODE_ENV: Joi.string().valid('development', 'production').required(),
});

Enter fullscreen mode Exit fullscreen mode

2.1.3 ConfigModule

Let's import ConfigModule in AppModule and use validationSchema which we created:

// ...
import { ConfigModule } from '@nestjs/config';
import { validationSchema } from './config/validation';

@Module({
  imports: [ConfigModule.forRoot({ validationSchema })],
  // ...
})
export class AppModule {}

Enter fullscreen mode Exit fullscreen mode

2.1.4 Ignore .env file

Make sure to add .env file in .gitignore:

.env
Enter fullscreen mode Exit fullscreen mode

2.2 CORS

We are going to allow API calls only from our client (WHITELIST_URL from above environment). CORS can help us achieve that.

2.2.1 CORS in NestJS

Under the hood, Nest makes use of the Express cors package. This package provides various options that you can customize based on your requirements.

Let's modify main.ts file:

// apps/api/src/main.ts

// ...
import { ConfigService } from '@nestjs/config';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  const configService = app.get(ConfigService);
  const allowList = configService.get<string>('WHITELIST_URL').split(',');
  const env = configService.get<string>('NODE_ENV');

  app.enableCors({
    origin: (origin, callback) => {
      // allow requests with no origin
      // (like mobile apps or curl requests)
      if (!origin && env !== 'production') return callback(null, true);

      if (allowList.indexOf(origin) === -1) {
        const msg =
          'The CORS policy for this site does not allow access from the specified Origin.';
        return callback(new Error(msg), false);
      }
      return callback(null, true);
    },
  });

  const globalPrefix = 'api';
  // rest remains same
}

bootstrap();

Enter fullscreen mode Exit fullscreen mode

2.3 API Redirection

Let's create an API at /github route.

2.3.1 AppController

// apps/api/src/app/app.controller.ts

// ...
@Controller()
export class AppController {
  constructor(
    private readonly appService: AppService
  ) {}

  // ...

  @Post('github')
  async github() {
    // TODO
  }
}

Enter fullscreen mode Exit fullscreen mode

2.3.2 The API logic

Below is what we want to do:

  1. Read body from request
  2. Read PAT and API_URL from environment
  3. Forward body to GitHub API with PAT attached in Authorization
  4. Return response from GitHub API

Let's first read environments:

// apps/api/src/app/app.controller.ts

// ...
import { ConfigService } from '@nestjs/config';

export class AppController {
  private token: string;
  private api_url: string;
  constructor(
    // ...
    private readonly configService: ConfigService
  ) {
    this.token = this.configService.get('PAT');
    this.api_url = this.configService.get<string>('API_URL');
  }
  //...
}

Enter fullscreen mode Exit fullscreen mode

To make request to GitHub API endpoint, we are going to need HttpService.

Axios is richly featured HTTP client package that is widely used. Nest wraps Axios and exposes it via the built-in HttpModule. The HttpModule exports the HttpService class, which exposes Axios-based methods to perform HTTP requests.

To use the HttpService, first import HttpModule.

@Module({
  imports: [HttpModule],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

Next, inject HttpService using normal constructor injection.

export class AppController {
  // ...
  constructor(
    // ...
    private httpService: HttpService
  ) {
    // ...
  }
  //...
}
Enter fullscreen mode Exit fullscreen mode

And now, the API function:

@Post('github')
async github(@Req() req: Request, @Res() res: Response) {
  const result = await this.httpService
    .post(this.api_url, req.body, {
      headers: { Authorization: 'Bearer ' + this.token },
    })
    .toPromise();
  res.status(result.status).send(result.data);
}
Enter fullscreen mode Exit fullscreen mode

That's it! Our NestJS API is ready to serve GitHub GraphQL.

3. Apollo Angular

I have used Apollo Angular as GraphQL client. Below are the steps:

3.1 Installation

npm install apollo-angular @apollo/client graphql
Enter fullscreen mode Exit fullscreen mode

3.2 Changes tsconfig.json

The @apollo/client package requires AsyncIterable so make sure your tsconfig.base.json includes esnext.asynciterable:

{
  "compilerOptions": {
    // ...
    "lib": [
      "es2017",
      "dom",
      "esnext.asynciterable"
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

3.3 API URL

Let's store the API URL in environments:

// apps/client/src/environments/environment.ts

export const environment = {
  production: false,
  api_url: '/api',
};

Enter fullscreen mode Exit fullscreen mode

NX created a proxy configuration that allows the Angular application to talk to the API in development.

To see how it works, open angular.json and find the serve target:

{
  "serve": {
    "builder": "@angular-devkit/build-angular:dev-server",
    "options": {
      "browserTarget": "client:build",
      "proxyConfig": "apps/client/proxy.conf.json"
    },
    "configurations": {
      "production": {
        "browserTarget": "client:build:production"
      }
    }
  },
}
Enter fullscreen mode Exit fullscreen mode

Note the proxyConfig property.

Now open proxy.conf.json:

{
  "/api": {
    "target": "http://localhost:3333",
    "secure": false
  }
}
Enter fullscreen mode Exit fullscreen mode

This configuration tells nx serve to forward all requests starting with /api to the process listening on port 3333.

Change it to http://localhost:3000, as we're running our API at 3000 port.

Next is prod API URL:

// apps/client/src/environments/environment.prod.ts

export const environment = {
  production: true,
  api_url: 'https://url-to-api/api',
};

Enter fullscreen mode Exit fullscreen mode

Make sure to replace url-to-api with actual URL where your API project is deployed.

3.4 GraphQL Module

Create a GraphQL module:

ng g m graphql --project=client
Enter fullscreen mode Exit fullscreen mode
// apps/client/src/app/graphql/graphql.module.ts

import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
import { Apollo, APOLLO_OPTIONS } from 'apollo-angular';
import { HttpLink } from 'apollo-angular/http';
import { InMemoryCache, ApolloLink } from '@apollo/client/core';
import { setContext } from '@apollo/client/link/context';
import { environment } from '../../environments/environment';

const uri = environment.api_url + '/github';

export function createApollo(httpLink: HttpLink) {
  const basic = setContext((operation, context) => ({
    headers: {
      Accept: 'charset=utf-8',
    },
  }));

  const link = ApolloLink.from([basic, httpLink.create({ uri })]);
  const cache = new InMemoryCache();

  return {
    link,
    cache,
  };
}

@NgModule({
  exports: [HttpClientModule],
  providers: [
    {
      provide: APOLLO_OPTIONS,
      useFactory: createApollo,
      deps: [HttpLink],
    },
  ],
})
export class GraphQLModule {}

Enter fullscreen mode Exit fullscreen mode

And import it in app.module.ts:

// apps/client/src/app/app.module.ts

// ...
@NgModule({
  // ...
  imports: [
    // ...
    GraphQLModule,
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

3.5 Services

In Apollo Angular, there are 3 APIs: Query, Mutation and Subscription. Each of them allows to define the shape of a result & variables. The only thing you need to do is to set the document property. That’s it, you use it as a regular Angular service.

In this approach GraphQL Documents are first-class citizens, you think about the query, for example, as a main subject. You can read more about it at Query, Mutation, Subscription services.

3.5.1 Code Generation

There's a tool to generate a ready to use in your component, strongly typed Angular services, for every defined query, mutation or subscription.

3.5.1.1 GraphQL Code Generator

GraphQL Code Generator is a CLI tool that can generate TypeScript typings out of a GraphQL schema.

First, we will install GraphQL Code Generator:

npm install --save-dev @graphql-codegen/cli
Enter fullscreen mode Exit fullscreen mode

Next, let's create a basic query at apps/client/src/.graphql:

query Viewer {
  viewer {
    login
  }
}
Enter fullscreen mode Exit fullscreen mode

GraphQL Code Generator lets you set up everything by simply running the following command:

npx graphql-codegen init
Enter fullscreen mode Exit fullscreen mode

Answer the questions:

$ npx graphql-codegen init

    Welcome to GraphQL Code Generator!
    Answer few questions and we will setup everything for you.

? What type of application are you building?           Application built with Angular

? Where is your schema?: (path or url)                 https://raw.githubusercontent.com/octokit/graphql-schema/master/schema.graphql

? Where are your operations and fragments?:            apps/client/src/.graphql

? Pick plugins:                                        TypeScript (required by other typescript plugins),
                                                       TypeScript Operations (operations and fragments),
                                                       TypeScript Apollo Angular (typed GQL services)

? Where to write the output:                           apps/client/src/generated/graphql.ts

? Do you want to generate an introspection file?       Yes

? How to name the config file?                         codegen.yml

? What script in package.json should run the codegen?  generate-codegen
Enter fullscreen mode Exit fullscreen mode

After above, below changes are done:

  1. codegen.yml is created at root of you project
  2. generate-codegen is added in scripts section of package.json
  3. apps/client/src/generated/graphql.ts is created with strongly typed Angular services, for every defined query, mutation or subscription

3.6 Usage

Now, it's time to consume Github APIs.

// apps/client/src/app/home/home.component.ts

// ...
import { ViewerGQL, ViewerQuery } from '../../generated/graphql';

@Component(...)
export class HomeComponent implements OnInit {
  // ..
  viewer$: Observable<ViewerQuery['viewer']>;

  constructor(
    // ...
    private viewerGQL: ViewerGQL
  ) {}

  ngOnInit() {
    this.viewer$ = this.viewerGQL.watch().valueChanges.pipe(map((result) => result.data.viewer));

    // subscribe to get the data
    this.viewer$.subscribe((data)=>console.log(data));
  }
}

Enter fullscreen mode Exit fullscreen mode

Cool, we're done with GraphQL related setup. You can run both the projects and check th results.

4 Deployment

We are going to create one more app on Digital Ocean for the API and connect the same repo. And we will also need some helper scripts to build the projects.

4.1 Scripts

First, install npm-run-all:

npm i -D npm-run-all
Enter fullscreen mode Exit fullscreen mode

And then add below scripts in package.json:

{
  // ...
  "scripts": {
    // ...
    "build:prod:client": "ng build client --prod",
    "build:prod:server": "nx build api --prod",
    "build:prod": "run-s build:prod:server build:prod:client",
    "deploy:start:client": "serve -s dist/apps/client -l tcp://0.0.0.0:$PORT -n",
    "deploy:start:server": "node dist/apps/api/main.js",
    "deploy:start": "run-p deploy:start:server deploy:start:client",
  },
}
Enter fullscreen mode Exit fullscreen mode

4.2 App Setup

I am going to list out app setup for both, API and Client.

4.2.1 Environment Variables

4.2.1.1 API
Variable Value
PAT GitHub Person Access Token. Also mark it as Encrypted
API_URL https://api.github.com/graphql
WHITELIST_URL https://url-of-your-client
NODE_ENV production

4.2.2 Commands

4.2.2.1 API
- Command
Build npm run build:prod:server
Run npm run deploy:start:server
4.2.2.2 Client
- Command
Build npm run build:prod:client
Run npm run deploy:start:client

5 Next Steps

Now I am going show data on client and charts with ng2-charts.

If you want to have look, checkout: https://gitalytics.shhdharmen.me/


Thanks for reading.

Happy Coding

🌲 🌞 😊

Discussion (0)