marp | theme | class | size |
---|---|---|---|
true |
default |
invert |
58140 |
During this course we will briefly go through these subjects:
- set up code-first GraphQL API with NestJS and TypeORM and PostgreSQL
- set up GraphQL web client using create-react-app and ApolloClient
- tie everything together with Docker, docker-compose and graphql-code-generator for a seamless local development experience
In the end we will have everything set up in such a way that code-changes in the backend will automatically update types and hooks used by the frontend components. This will make the development experience between frontend and backend development seamless and easy.
- Improved development experience:
- automatic type and hook generation for frontend based on backend types
- built-in query/input validation errors for API
- built-in documentation for API
- designed from the start to serve clients with different needs: no more custom REST-like APIs for doing complex queries to the database
- REST is industry standard and might be required by external APIs
- HTTP caching is harder to implement since all queries are POST by default
- Mitigation: Configure Apollo to use GET queries
- Possible N + 1 query problems when serving relational data
- ???
Tools used:
- Docker
- a terminal
- a text editor (or IDE)
- a browser
Make sure you have these installed and configured before starting the course.
Knowledge of the following subjects is beneficial, although not required:
- Node
- React
- TypeScript
- yarn (feel free to use npm+npx instead)
- NestJS (this will be used to get us started quickly and avoid dealing with Express)
- TypeORM (this will be used to avoid dealing with SQL)
The main focus will be on these:
- GraphQL
- ApolloClient
- each step in this course is documented as a separate commit that can be accessed by viewing the associated pull request:
- the slides will go through each step and present how some the code changes can be created with command line tools
- some slides will contain a context quote to help keep tract the expected state of the terminal:
Context:
/graphql-training && docker-compose up -d
Install NestJS CLI globally
yarn global add @nestjs/cli
Clone the repo and checkout code in the starter
branch:
git clone https://github.com/wunderdogsw/graphql-training.git
cd graphql-training
git checkout starter
git checkout -b local
Create an API using NestJS:
nest new api --package-manager yarn
Create a web app using CRA:
yarn create react-app web --template typescript
Use the configuration provided in docker-compose.yml
to start the API, the web app and the DB:
docker-compose up -d
Check that the API works at http://localhost:8000
Check that the web app works at http://localhost:3000
Feel free to also check that the DB works at http://localhost:5432
Troubleshooting and shutdown:
docker ps
docker-compose logs api
docker-compose logs web
docker-compose logs db
docker-compose down
Context:
/graphql-training && docker-compose up -d
Check that the API works at http://localhost:8000
Context:
/graphql-training/api
Add GraphQL to the API
yarn add @nestjs/graphql apollo-server-express graphql-tools graphql
Add resources related to a todo entity:
nest generate resource todo
- Choose
GraphQL (code first)
- Choose
Y
for CRUD
Modify /graphql-training/api/src/app.module.ts
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { join } from 'path';
import { TodoModule } from './todo/todo.module';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [
GraphQLModule.forRoot({
autoSchemaFile: join(process.cwd(), 'src/schema.graphql'),
playground: true,
}),
TodoModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Context:
/graphql-training && docker-compose up -d
Open browser to access playground: http://localhost:8000/graphql
query {
todo(id: 1) {
exampleField
}
}
This will result in an error, since the default resolver is not ready (and also there is no data).
Context:
/graphql-training && docker-compose up -d
Make changes:
/src/todo/dto/create-todo.input.ts
/src/todo/dto/update-todo.input.ts
/src/todo/entities/todo.entity.ts
/src/todo/todo.resolver.ts
/src/todo/todo.service.ts
Observe that:
/src/schema.graphql
is automatically updated (code first configuration)- the documentation is available in the playground
See commit: API: Update todo models and service to return sane results
Context:
/graphql-training && docker-compose up -d
Create mutation:
mutation {
createTodo(createTodoInput: {
description: "Create todo"
}) {
id
description
}
}
- observe when trying to input something bad => GRAPHQL_VALIDATION_FAILED!
Context:
/graphql-training && docker-compose up -d
Query all:
query {
todos {
id
description
}
}
Context:
/graphql-training/api
yarn add @nestjs/typeorm typeorm pg
TypeORM requires some changes to /graphql-training/api/tsconfig.json
:
{
"compilerOptions": {
"Leave other options": "as they were",
"esModuleInterop": true,
"moduleResolution": "node"
}
}
Add /graphql-training/api/ormconfig.ts
:
import { join } from 'path';
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
const config: TypeOrmModuleOptions = {
type: 'postgres',
host: process.env.POSTGRES_HOST,
port: Number(process.env.POSTGRES_PORT),
database: process.env.POSTGRES_DB,
username: process.env.POSTGRES_USER,
password: process.env.POSTGRES_PASSWORD,
synchronize: true,
logging: false,
entities: [join(__dirname, '**', '*.entity.{ts,js}')],
migrations: [join(__dirname, '**', '*.migration.{ts,js}')],
subscribers: [join(__dirname, '**', '*.subscriber.{ts,js}')],
};
export default config;
Context:
/graphql-training/api/src
Make changes:
/app.module.ts
: add TypeORM configuration/todo/entities/todo.entity.ts
: add TypeORM decorators/todo/todo.module.ts
: Todo configuration- this enables using
@InjectRepository(Todo)
- this enables using
/todo/todo.resolver.ts
/todo/todo.service.ts
: UseRepository<Todo>
to communicate with db
See commit API: Use db to store todo items
Feel free to check data in DB to check that everything is configured properly at this stage. See /graphql-training/docker-compose.yml
for details on DB configuration.
Context:
/graphql-training && docker-compose up -d
Check that the web app works at http://localhost:3000
- clean up generated code at will
Context:
/graphql-training/web
yarn add graphql @apollo/client
Create /graphql-training/web/src/client.ts
import { ApolloClient, InMemoryCache } from "@apollo/client";
export const client = new ApolloClient({
uri: "http://localhost:8000/graphql",
cache: new InMemoryCache(),
});
Create /graphql-training/web/src/components/Todo/Todo.tsx
import React from 'react';
import { useQuery, gql } from '@apollo/client';
type TodoItem = {
id: number;
description: string;
};
type Data = {
todos: TodoItem[];
};
const TODO_QUERY = gql`
query {
todos {
id
description
}
}
`;
const Todo: React.FC = () => {
const { loading, error, data } = useQuery<Data>(TODO_QUERY);
if (loading) return <p>Loading...</p>;
if (error || !data?.todos) return <p>Error!</p>;
return (
<ul>
{data.todos.map(({ id, description }) => (
<li key={id}>
Item {id}: {description}
</li>
))}
</ul>
);
};
export default Todo;
Change /graphql-training/web/src/App.tsx
:
import { ApolloProvider } from '@apollo/client';
import React from 'react';
import { client } from './client';
import Todo from './components/Todo/Todo';
const App: React.FC = () => (
<ApolloProvider client={client}>
<Todo />
</ApolloProvider>
);
export default App;
Context:
/graphql-training/web
yarn add --dev @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-react-apollo @graphql-codegen/typescript-apollo-client-helpers
Add graphql-codegen configuration: /graphql-training/web/codegen.yml
:
overwrite: true
schema: http://localhost:8000/graphql
documents: src/operations/**/*.graphql
generates:
src/types/generated-types-and-hooks.ts:
plugins:
- typescript
- typescript-operations
- typescript-react-apollo
- typescript-apollo-client-helpers
Create /graphql-training/web/src/operations/todo/list.graphql
for listing query:
query Todos {
todos {
id
description
}
}
Change /graphql-training/web/package.json
:
{
"Leave other options": "as they were",
"scripts": {
"Leave other scripts": "as they were",
"generate": "graphql-codegen",
"generate:watch": "graphql-codegen --watch"
}
}
Context:
/graphql-training && docker-compose up -d && cd web
yarn generate
This generates the types and hooks automatically based on what the API responds. (That's why the API must be running.) Now we can use the generated types and hooks directly in the components!
Modify /graphql-training/web/src/components/Todo/Todo.tsx
:
import React from 'react';
import { useTodosQuery } from '../../types/generated-types-and-hooks';
const Todo: React.FC = () => {
const { loading, error, data } = useTodosQuery();
if (loading) return <p>Loading...</p>;
if (error || !data?.todos) return <p>Error!</p>;
return (
<ul>
{data.todos.map(({ id, description }) => (
<li key={id}>
Item {id}: {description}
</li>
))}
</ul>
);
};
export default Todo;
The last, definitely optional, step that we will take to improve the development flow is to automatically update the frontend types when the backend types change.
Modify the schema in /graphql-training/web/codegen.yml
:
schema: ../api/src/schema.graphql
Context:
/graphql-training/web && docker-compose down
yarn add --dev concurrently
Change /graphql-training/web/package.json
:
{
"Leave other options": "as they were",
"scripts": {
"start": "concurrently \"yarn generate:watch\" \"react-scripts start\"",
"Leave other scripts": "as they were"
}
}
Make sure graphql-codegen
can access the API schema by changing /graphql-training/docker-compose.yml
:
services:
// Leave other options as they were
web:
// Leave other options as they were
volumes:
- ./web:/usr/src/app
- ./api/src/schema.graphql:/usr/src/api/src/schema.graphql
Context:
/graphql-training && docker-compose up
-
Make changes to
/graphql-training/api/src/todo/entities/todo.entity.ts
and save them. -
First nest will trigger a new build and the autoSchemaFile configuration in
/graphql-training/api/src/app.module.ts
will trigger the update of the API schema/graphql-training/api/src/schema.graphql
:
[10:37:03 AM] File change detected. Starting incremental compilation...
...
api_1 | [Nest] 74 - 01/18/2021, 10:37:05 AM [NestFactory] Starting Nest application...
...
api_1 | [Nest] 74 - 01/18/2021, 10:37:05 AM [NestApplication] Nest application successfully started +161ms
- Next,
graphql-codegen
will trigger from the API schema update and generate new types and hooks for the frontend:
web_1 | [0] [10:37:07] Parse configuration [started]
...
web_1 | [0] [10:37:07] Generate src/types/generated-types-and-hooks.ts [completed]
...
web_1 | [1] Compiled successfully!
Congratulations! You have just implemented an API and a client that have types synced. Additionally the API provides validation out of the box and is well documented.
Context:
/graphql-training
mkdir slides
cd slides
yarn init
yarn add --dev @marp-team/marp-cli
Modify /graphql-training/slides/package.json
:
{
"Leave other options": "as they were",
"scripts": {
"slides": "marp '../README.md' -o slides.html",
"slides:watch": "marp '../README.md' -o slides.html --watch"
}
}
Add the following in the beginning of /graphql-training/README.md
:
---
marp: true
theme: default
class: invert
---
Context:
/graphql-training/slides
yarn slides