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...
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
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(),
});
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 {}
2.1.4 Ignore .env file
Make sure to add .env file in .gitignore:
.env
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();
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
}
}
2.3.2 The API logic
Below is what we want to do:
- Read body from request
- Read
PAT
andAPI_URL
from environment - Forward body to GitHub API with PAT attached in Authorization
- 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');
}
//...
}
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 {}
Next, inject HttpService using normal constructor injection.
export class AppController {
// ...
constructor(
// ...
private httpService: HttpService
) {
// ...
}
//...
}
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);
}
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
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"
]
}
}
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',
};
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"
}
}
},
}
Note the proxyConfig property.
Now open proxy.conf.json:
{
"/api": {
"target": "http://localhost:3333",
"secure": false
}
}
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',
};
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
// 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 {}
And import it in app.module.ts
:
// apps/client/src/app/app.module.ts
// ...
@NgModule({
// ...
imports: [
// ...
GraphQLModule,
],
bootstrap: [AppComponent],
})
export class AppModule {}
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
Next, let's create a basic query at apps/client/src/.graphql:
query Viewer {
viewer {
login
}
}
GraphQL Code Generator lets you set up everything by simply running the following command:
npx graphql-codegen init
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
After above, below changes are done:
- codegen.yml is created at root of you project
-
generate-codegen
is added inscripts
section of package.json - 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));
}
}
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
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",
},
}
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
π² π π
Top comments (0)