by Gion Kunz & Tomas Trajan
Founder, UI Developer, UX Enthusiast at syncrea GmbH
Angular focused Software Consultant, Speaker, Trainer, Blogger
Next Meetup, Tomorrow 25. September 2019:
"How to generate Angular REST Clients with OpenAPI Generator"
Complete local setup to create NodeJS projects and prepare for Angular and the Apollo Platform.
Apollo is a platform for building a data graph, a communication layer that seamlessly connects application clients (such as React and iOS apps) to back-end services.
Create a GraphQL server with Apollo Server and Express.
# Create project folder
mkdir graphql-todo && cd graphql-todo
# Create sub-folder for our server
mkdir graphql-todo-server && cd graphql-todo-server
npm init
# Install typescript, nodemon and node-ts
npm i typescript nodemon ts-node --save-dev
# Initialize TypeScript project
npx tsc --init --rootDir src --outDir dist --lib dom,es6 --module commonjs --removeComments
# Apollo Platform and Server dependencies
npm i apollo-server-express cors express graphql http ncp uuid --save
# Typing files for Node.js and dependencies
npm i @types/express @types/graphql @types/uuid @types/node graphql-import graphql-import-node --save-dev
// package.json
// ...
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "nodemon \"src/index.ts\" --exec \"ts-node\" src/index.ts -e ts,graphql"
},
// ...
// src/index.ts
import express from 'express';
import {ApolloServer} from 'apollo-server-express';
import {createServer} from 'http';
import cors from 'cors';
import {schema} from './schema';
const host = '0.0.0.0';
const port = 8080;
const app = express();
const server = new ApolloServer({schema});
app.use('*', cors());
server.applyMiddleware({app, path: '/graphql'});
const httpServer = createServer(app);
httpServer.listen(
{host, port},
(): void => console.log(`\nš GraphQL is now running on http://localhost:${port}/graphql`)
);
// src/schema.graphql
type Query {
helloWorld: String!
}
// src/schema.ts
import 'graphql-import-node';
import * as typeDefs from './schema.graphql';
import {makeExecutableSchema} from 'graphql-tools';
import {GraphQLSchema} from 'graphql';
import {IResolvers} from 'graphql-tools';
export const resolvers: IResolvers = {
Query: {
helloWorld() {
return `š Hello world! š`;
}
}
};
export const schema: GraphQLSchema = makeExecutableSchema({
typeDefs,
resolvers
});
Angular
React
Vue.js
Component Rendering
Router
TypeScript Friendliness
Animation
Multi-Platform
Server Side Rendering
State Management
Core Support
Core Support
Core Support
Written in TypeScript
TypeScript JSX and good support
Good support
Core Support
react-router
vue-router
Core Support
react-move, react-motion and others
Core Support
vue-server-renderer
Core Support
Virtual DOM, react native
Render Adapter, Web Worker, Server, Native
Weex, supported but limited
Virual DOM, roll-your-own, Next.js
ngrx store 3rd party
Redux 3rd party
vuex
Create our first Angular App using the Angular CLI.
# Install Angular CLI as global module
npm i @angular/cli --global
# Create todo app (in the graphql-todo folder!)
ng new graphql-todo-app
# ? Would you like to add Angular routing? No
# ? Which stylesheet format would you like to use? CSS
# Enter folder and serve app
cd graphql-todo-app && ng serve
# Using the CLI we can add ng addable libraries
ng add @angular/material
# ? Choose a prebuilt theme name, or "custom" for a custom theme: Indigo/Pink
# ? Set up HammerJS for gesture recognition? Yes
# ? Set up browser animations for Angular Material? Yes
// src/app/app.module.ts
...
import {MatListModule, MatToolbarModule, MatFormFieldModule, MatCheckboxModule, MatInputModule, MatButtonModule} from '@angular/material';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
BrowserAnimationsModule,
MatToolbarModule,
MatButtonModule,
MatInputModule,
MatFormFieldModule,
MatCheckboxModule,
MatListModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
<!-- src/app/app.component.html -->
<mat-toolbar class="header">
<span role="header" color='primary'>GraphQL Todo</span>
</mat-toolbar>
<main class="container">
<div class="add-todo">
<mat-form-field class="add-todo-field">
<input #todoInput matInput
(keyup.enter)="addTodo(todoInput.value); todoInput.value = ''"
placeholder="Today, I learn about Angular, GraphQL and TypeScript!">
</mat-form-field>
<button mat-mini-fab (click)="addTodo(todoInput.value); todoInput.value = ''">
<i class="material-icons">add</i>
</button>
</div>
<mat-list>
<mat-list-item *ngFor="let todo of todoItems" class="list">
<mat-checkbox color='primary'>
{{ todo }}
</mat-checkbox>
</mat-list-item>
</mat-list>
</main>
// src/app/app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
todoItems = ['Go for a walk with my dog', 'Buy some bread and butter', 'Learn how to paint'];
addTodo(title: string) {
this.todoItems = [title, ...this.todoItems];
}
}
Connect our Angular App to our Apollo GraphQL server.
# Install graphql-cli for tooling
npm i graphql-cli --global
# Initialize graphql client project (within graphql-todo-app folder)
graphql init
# ? Enter project name (Enter to skip):
# ? Local schema file path: ../graphql-todo-server/src/schema.graphql
# ? Endpoint URL (Enter to skip): http://localhost:8080/graphql
# ? Name of this endpoint, for e.g. default, dev, prod: default
# ? Subscription URL (Enter to skip):
# ? Do you want to add other endpoints? No
# ? What format do you want to save your config in? JSON
# Install Apollo Angular
ng add apollo-angular
// src/app/graphql.module.ts
import {NgModule} from '@angular/core';
import {ApolloModule, APOLLO_OPTIONS} from 'apollo-angular';
import {HttpLinkModule, HttpLink} from 'apollo-angular-link-http';
import {InMemoryCache} from 'apollo-cache-inmemory';
const uri = 'http://localhost:8080/graphql';
export function createApollo(httpLink: HttpLink) {
return {
link: httpLink.create({uri}),
cache: new InMemoryCache(),
};
}
@NgModule({
exports: [ApolloModule, HttpLinkModule],
providers: [
{
provide: APOLLO_OPTIONS,
useFactory: createApollo,
deps: [HttpLink],
},
],
})
export class GraphQLModule {}
// src/app/app.component.ts
import {Component} from '@angular/core';
import {Apollo} from 'apollo-angular';
import gql from 'graphql-tag';
const helloWorld = gql`
{
helloWorld
}
`;
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
todoItems = ['Go for a walk with my dog', 'Buy some bread and butter', 'Learn how to paint'];
message: string;
constructor(private apollo: Apollo) {
this.apollo
.watchQuery<{helloWorld: string}>({query: helloWorld})
.valueChanges
.subscribe(result => {
this.message = result.data.helloWorld;
});
}
addTodo(title: string) {
this.todoItems = [title, ...this.todoItems];
}
}
Implement Todo App Client and Server using Todo model and updated GraphQL Schema.
// graphql-todo-server/src/schema.graphql
type Query {
getTodos: [Todo!]!
}
type Todo {
id: ID!
title: String!
done: Boolean!
}
// graphql-todo-server/src/schema.ts
import 'graphql-import-node';
import * as typeDefs from './schema.graphql';
import {makeExecutableSchema} from 'graphql-tools';
import {GraphQLSchema} from 'graphql';
import {IResolvers} from 'graphql-tools';
export interface Todo {
id: string;
title: string;
done: boolean;
}
export const inMemoryDb: {todos: {[id: string]: Todo}} = {
todos: {
'1': {
id: '1',
title: 'Todo 1',
done: false
}
}
}
export const resolvers: IResolvers = {
Query: {
getTodos(): Todo[] {
return Object.values(inMemoryDb.todos);
}
}
};
export const schema: GraphQLSchema = makeExecutableSchema({
typeDefs,
resolvers
});
// graphql-todo-app/src/app/app.component.ts
import { Component } from '@angular/core';
import {Apollo} from 'apollo-angular';
import gql from 'graphql-tag';
import {Todo} from '../../../graphql-todo-server/src/schema';
const allTodosQuery = gql`
query {
getTodos {
id
title
done
}
}
`;
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
todoItems: Todo[] = [];
constructor(private apollo: Apollo) {
this.apollo
.watchQuery<{getTodos: Todo[]}>({
query: allTodosQuery
})
.valueChanges
.subscribe(result => {
this.todoItems = result.data.getTodos;
});
}
addTodo(title: string) {
// TODO: This will be implemented later
}
}
// graphql-todo-server/src/schema.graphql
type Query {
getTodos: [Todo!]!
}
type Mutation {
addTodo(title: String!, done: Boolean! = false): [Todo!]!
updateTodo(id: ID!, done: Boolean!): [Todo!]!
}
type Todo {
id: ID!
title: String!
done: Boolean!
}
// graphql-todo-server/src/schema.ts
import 'graphql-import-node';
import * as typeDefs from './schema.graphql';
import {makeExecutableSchema} from 'graphql-tools';
import {GraphQLSchema} from 'graphql';
import {IResolvers} from 'graphql-tools';
import uuid from 'uuid/v4';
export interface Todo {
id: string;
title: string;
done: boolean;
}
export const inMemoryDb: {todos: {[id: string]: Todo}} = {
todos: {
'1': {
id: '1',
title: 'Todo 1',
done: false
}
}
}
export const resolvers: IResolvers = {
Query: {
getTodos(): Todo[] {
return Object.values(inMemoryDb.todos);
}
},
Mutation: {
addTodo(_, {title, done = false}: Todo): Todo[] {
const id: string = uuid();
inMemoryDb.todos = {
...inMemoryDb.todos,
[id]: {
id,
title,
done
}
};
return Object.values(inMemoryDb.todos);
},
updateTodo(_, {id, done}: {id: string, done: boolean}): Todo[] {
inMemoryDb.todos = {
...inMemoryDb.todos,
[id]: {
...inMemoryDb.todos[id],
id,
done
}
};
return Object.values(inMemoryDb.todos);
}
}
};
export const schema: GraphQLSchema = makeExecutableSchema({
typeDefs,
resolvers
});
// graphql-todo-app/src/app/app.component.ts
import { Component } from '@angular/core';
import {Apollo} from 'apollo-angular';
import gql from 'graphql-tag';
import {Todo} from '../../../graphql-todo-server/src/schema';
const allTodosQuery = gql`
query {
getTodos {
id
title
done
}
}
`;
const addTodoMutation = gql`
mutation addTodo($title: String!, $done: Boolean! = false) {
addTodo(title: $title, done: $done) {
id
title
done
}
}
`;
const updateTodoMutation = gql`
mutation updateTodo($id: ID!, $done: Boolean!) {
updateTodo(id: $id, done: $done) {
id
title
done
}
}
`;
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
todoItems: Todo[] = [];
constructor(private apollo: Apollo) {
this.apollo
.watchQuery<{getTodos: Todo[]}>({
query: allTodosQuery
})
.valueChanges
.subscribe(result => {
this.todoItems = result.data.getTodos;
});
}
addTodo(title: string) {
this.apollo.mutate<{addTodo: Todo[]}>({
mutation: addTodoMutation,
variables: {
title
}
}).subscribe(result => {
this.todoItems = result.data.addTodo;
})
}
updateTodo(id: string, done: boolean) {
this.apollo.mutate<{updateTodo: Todo[]}>({
mutation: updateTodoMutation,
variables: {
id,
done
}
}).subscribe(result => {
this.todoItems = result.data.updateTodo;
})
}
}
<!-- graphql-todo-app/src/app/app.component.html -->
<mat-toolbar class="header">
<span role="header" color='primary'>GraphQL Todo</span>
</mat-toolbar>
<main class="container">
<div class="add-todo">
<mat-form-field class="add-todo-field">
<input #todoInput matInput
(keyup.enter)="addTodo(todoInput.value); todoInput.value = ''"
placeholder="Today, I learn about Angular, GraphQL and TypeScript!">
</mat-form-field>
<button mat-mini-fab (click)="addTodo(todoInput.value); todoInput.value = ''">
<i class="material-icons">add</i>
</button>
</div>
<mat-list #todoist class="todolist">
<mat-list-item *ngFor="let todo of todoItems" class="list">
<mat-checkbox color='primary'
[checked]="todo.done"
(change)="updateTodo(todo.id, !todo.done)">
{{ todo.title }}
</mat-checkbox>
</mat-list-item>
</mat-list>
</main>