Chapter 2: TypeGraphQL
Full Stack TypeScript
link TypeGraphQL
By the end of this lesson, developers will be able to:
- Create database entities using TypeORM
- Create queries and resolvers with TypeGraphQL
- Respond to a query with data from a resolver
Introduction
In this section, we begin to create a GraphQL backend using TypeGraphQL. With TypeGraphQL, we can utilize TypeScript and GraphQL to create static types and queries for our database. Once we complete this setup, we will generate queries using GraphQL Code Generator.
To generate and manage our database models we use TypeORM. TypeORM is an Object Relational Mapping for TypeScript, and is supported by the TypeGraphQL libray.
link Overview
Before getting started, take a tour of these libraries:
Let's get started!
We shall begin building our TypeGraphQL server by installing the TypeORM CLI, and running the init
command:
npx typeorm init --name travel-api --database postgresql
cd travel-api
yarn
link TypeGraphQL
Then, switch into your new directory and install these dependencies:
yarn add apollo-server-express class-transformer connect-sqlite3 express express-session graphql graphql-toolkit pg sqlite3 type-graphql
Adding pg
and apollo-server-express
will allow us to develop an Apollo Server built on top of Express with a PostgresQL database. We utlize sqlite3
for local development.
Let's also add the necessary dev dependencies for Express.
yarn add -D ts-node-dev nodemon @types/express-session @types/express
link TypeORM Config
For local development we are going to use sqlite3
. This means our database will be inside of a file called database.sqlite
.
Rename ormconfig.json
, to ormconfig.js
, and populate it with the following:
module.exports = [
{
name: "development",
type: "sqlite",
database: "database.sqlite",
synchronize: true,
logging: true,
entities: ["src/entity/**/*.ts"],
migrations: ["src/migration/**/*.ts"],
subscribers: ["src/subscriber/**/*.ts"],
cli: {
entitiesDir: "src/entity",
migrationsDir: "src/migration",
subscribersDir: "src/subscriber"
}
},
{
name: "production",
type: "postgres",
url: process.env.DATABASE_URL,
synchronize: false, // switch this to false once you have the initial tables created and use migrations instead
logging: false,
entities: ["dist/entity/**/*.js"],
migrations: ["dist/migration/**/*.js"],
subscribers: ["dist/subscriber/**/*.js"],
cli: {
entitiesDir: "dist/entity",
migrationsDir: "dist/migration",
subscribersDir: "dist/subscriber"
}
}
];
link TypeScript Config
We configure TypeScript with custom compiler options. Let's modify tsconfig.json
with the following:
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"lib": ["dom", "es6", "es2017", "esnext.asynciterable"],
"sourceMap": true,
"outDir": "./dist",
"moduleResolution": "node",
"removeComments": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitThis": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"resolveJsonModule": true,
"baseUrl": "."
},
"exclude": ["node_modules", "./src/generated"],
"include": ["./src/**/*.tsx", "./src/**/*.ts"]
}
link Simple Server
We are getting started with the TypeScript server functionality, so now would be a great time to test things out. Let's modify our src/index.ts
with an async
function called bootstrap
.
- Note that we imported
getConnectionOptions
and createddbOptions
using thedefault
option.
import 'reflect-metadata';
import { createConnection, getConnectionOptions } from 'typeorm';
import { User } from './entity/User';
async function bootstrap() {
// get options from ormconfig.js
const dbOptions = await getConnectionOptions(
process.env.NODE_ENV || 'development'
);
createConnection({ ...dbOptions, name: 'default' })
.then(async connection => {
console.log('Inserting a new user into the database...');
const user = new User();
user.firstName = 'Timber';
user.lastName = 'Saw';
user.age = 25;
await connection.manager.save(user);
console.log('Saved a new user with id: ' + user.id);
console.log('Loading users from the database...');
const users = await connection.manager.find(User);
console.log('Loaded users: ', users);
console.log(
'Here you can setup and run express/koa/any other framework.'
);
})
.catch(error => console.log(error));
}
bootstrap();
Each time the server boots up, one new user is added and we see a full list of user
. Try changing the new user's first name, last name, or age.
We also see the raw SQL
queries executed on launch. This is because we have an option called logging
set to true
in our ormconfig.js
.
TypeORM is handling the databse setup, and performing migrations on the fly upon startup. We can see the results for each User
entity.
link Place Entity
Entities allow us to create and update database models. We can now try adding our first model: Place
.
Create a new file called src/entity/Place.ts
and insert the following:
import { Entity, BaseEntity, PrimaryGeneratedColumn, Column } from 'typeorm';
import { ObjectType, Field, ID } from 'type-graphql';
@ObjectType({ description: 'Destination or place of interest' })
@Entity()
export class Place extends BaseEntity {
@Field(() => ID)
@PrimaryGeneratedColumn()
id: number;
@Field()
@Column()
title: string;
@Field({
nullable: true,
description: 'The place description'
})
@Column()
description?: string;
@Field({
nullable: true,
description: 'The place image url'
})
@Column()
imageUrl?: string;
@Field({ nullable: true })
@Column()
creationDate?: Date;
}
Display Entity
If we wanted to test out our new entity, we could try to generate a few sample places for the database.
import { Place } from './entity/Place';
{/* ... */}
createConnection({ ...dbOptions, name: 'default' })
.then(async connection => {
console.log('Inserting a new place into the database...');
const place = new Place();
place.title = 'New York';
place.description = 'The Big Apple';
place.imageUrl = 'https://images.unsplash.com/photo-1485738422979-f5c462d49f74'
place.creationDate = new Date();
await connection.manager.save(place);
console.log('Saved a new place with id: ' + place.id);
console.log('Loading places from the database...');
const places = await connection.manager.find(Place);
console.log('Loaded places: ', places);
console.log(
'Here you can setup and run express/koa/any other framework.'
);
})
.catch(error => console.log(error));
link Place Input
In order to define and validate our TypeGraphQL resolver, we create a new file called graphql-types/PlaceInput.ts
. This file contains any required parameters for our first TypeGraphQL entity, Place.ts
.
mkdir src/graphql-types
touch src/graphql-types/PlaceInput.ts
Let's modify graphql-types/PlaceInput.ts
to provide the arguments for adding a new place.
import { InputType, Field } from 'type-graphql';
@InputType()
export class PlaceInput {
@Field({ nullable: true })
id?: number;
@Field()
title: string;
@Field({ nullable: true })
description?: string;
@Field({ nullable: true })
imageUrl?: string;
}
link Place Resolver
In order to server responses from a TypeGraphQL server, we will set up our first TypeGraphQL resolver. Resolvers serve data related to entities, so we can create a new file called resolvers/PlaceResolver.ts
.
mkdir src/resolvers
touch src/resolvers/PlaceResolver.ts
Let's modify resolvers/PlaceResolver.ts
to provide queries and mutations for creating and fetching new places:
import { Resolver, Query, Arg, Mutation } from 'type-graphql';
import { plainToClass } from 'class-transformer';
import { Place } from '../entity/Place';
import { PlaceInput } from '../graphql-types/PlaceInput';
@Resolver(() => Place)
export class PlaceResolver {
@Query(() => Place, { nullable: true })
async place(@Arg('id') id: number): Promise<Place | undefined> {
return await Place.findOne(id);
}
@Query(() => [Place], {
description: 'Get all the places from around the world '
})
async places(): Promise<Place[]> {
const places = await Place.find();
return places;
}
@Mutation(() => Place)
async createPlace(@Arg('place') placeInput: PlaceInput): Promise<Place> {
const place = plainToClass(Place, {
description: placeInput.description,
title: placeInput.title,
imageUrl: placeInput.imageUrl,
creationDate: new Date()
});
const newPlace = await Place.create({ ...place }).save();
return newPlace;
}
}
@Query
and @Mutation
are useful decorators for our first @Resolver
class. A decorator assumes certain properties on a given class or function. So in this case, we add decorators to our resolver's three functions:
async place(@Arg('id') id: number): Promise<Place | undefined>
: Returns a single place based on it'sID
, or undefined.async places(): Promise<Place[]>
: Returns a list of all places, or an empty array.async createPlace(@Arg('place') placeInput: PlaceInput): Promise<Place>
: Adds and returns a new place usingPlaceInput
parameters.
link Apollo Server
Now that we defined our first TypeGraphQL resolver, we are going to initialize a new ApolloServer
. Let's modify the src/index.ts
to include the apollo server.
Starting at the top level imports, add the following modules:
import 'reflect-metadata';
import { createConnection, getConnectionOptions } from 'typeorm';
import express from 'express';
import session from 'express-session';
import connectSqlite3 from 'connect-sqlite3';
import { ApolloServer } from 'apollo-server-express';
import * as path from 'path';
import { buildSchema } from 'type-graphql';
import { PlaceResolver } from './resolvers/PlaceResolver';
const SQLiteStore = connectSqlite3(session);
You are going to see an issue related to connect-sqlite3
, this is acceptable - we will resolve it soon.
Up next, add an express
instance and connect it to your sqlite3
database:
async function bootstrap() {
const app = express();
// use express session
app.use(
session({
store: new SQLiteStore({
db: 'database.sqlite',
concurrentDB: true
}),
name: 'qid',
secret: process.env.SESSION_SECRET || 'aslkdfjoiq12312',
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
maxAge: 1000 * 60 * 60 * 24 * 7 * 365 // 7 years
}
})
);
// get options from ormconfig.js
const dbOptions = await getConnectionOptions(
process.env.NODE_ENV || 'development'
);
createConnection({ ...dbOptions, name: 'default' })
.then(async () => {
/* Initialize apollo server here */
})
.catch(error => console.log(error));
}
bootstrap();
Now that we initialized a server connection, we can create the ApolloServer
instance. Modify the callback function of createConnection
with the following:
/* Initialize apollo server here */
// build TypeGraphQL executable schema
const schema = await buildSchema({
// add all typescript resolvers
// __dirname + '/resolvers/*.ts'
resolvers: [PlaceResolver],
validate: true,
// automatically create `schema.gql` file with schema definition in current folder
emitSchemaFile: path.resolve(__dirname, 'schema.gql')
});
// Create GraphQL server
const apolloServer = new ApolloServer({
schema,
context: ({ req, res }) => ({ req, res }),
introspection: true,
// enable GraphQL Playground
playground: true
});
apolloServer.applyMiddleware({ app, cors: true });
const port = process.env.PORT || 4000;
// Start the server
app.listen(port, () => {
console.log(`Server started at http://localhost:${port}/graphql`);
});
Inside the callback function, we build our server schema and then initialize a new ApolloServer
instance. Once the server starts we allow cors
and run it on an associated port.
GraphQL Schema
Our GraphQL schema defines two unique properties: typeDefs
and resolvers
. Let's dig into their details:
typeDefs
define what data types are accessible in the server. In this case, we create unique Entity models that act as our GraphQL type definitions.resolvers
will execute logic based on theTypeORM
entity models. In this case, we used an@Resolver
decorator to associate resolvers with queries and mutations.
There's only one small issue...
link Custom Types
Sometimes your project modules include an unsupported type. In this case we have a module named connect-sqlite3
. Let's add a custom module declaration to begin running the server.
mkdir src/@types
touch src/@types/connect-sqlite3.d.ts
Inside your new src/@types/connect-sqlite3.d.ts
insert the following snippet:
declare module 'connect-sqlite3';
This makes the TypeScript compiler happy, and now we can run the server!
Test Server
Modify your package.json file to support ts-node-dev
.
"scripts": {
"start": "ts-node src/index.ts",
"dev": "ts-node-dev --respawn src/index.ts"
},
Let's make sure our server works:
yarn start
Open http://localhost:4000/graphql
, you should see a playground for your GraphQL endpoints.
Insert the following query to see your list of places:
query {
places {
id
title
description
imageUrl
}
}
If you see the list of places, well done!
Now let's try a new place mutation:
mutation CreatePlace {
createPlace(place: {
title: "Central Park"
description: "The Great Lawn",
imageUrl: "https://images.unsplash.com/photo-1568515387631-8b650bbcdb90"
}) {
title
description
imageUrl
creationDate
}
}
And try fetching a specific place:
query GetPlace {
place(id: 1) {
title
description
imageUrl
creationDate
}
}
We now have an API with some CRUD functionality built in.
Well Done!
Wrapping Up
Today we made a TypeGraphQL server which displays and creates places. The next chapter introduces GraphQL Codegen, to automate our frontend network requests!