- Published on
TypeGraphQL is a popular library to help you create a GraphQL server, written in TypeScript.
It works very well with TypeORM to access database data.
Initial setup
Before we being, we need to set a few things up. You can skip this section if you are not following along with the code changes.
Postgres (docker-compose)
Here is a docker-compose config to set up Postgres:
version: '3.5'
services:
graphql-database:
image: postgres
container_name: graphql-database
environment:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
volumes:
- ./postgresql-data:/var/lib/postgresql/data
ports:
- '5432:5432'
Run it with docker-compose up
.
TypeORM config
Config file to connect to the docker postgres db:
{
"name": "default",
"type": "postgres",
"host": "localhost",
"port": 5432,
"username": "postgres",
"password": "postgres",
"database": "postgres",
"synchronize": false,
"logging": "all",
"logger": "advanced-console",
"entities": ["src/entity/**/*.*"],
"cli": {
"entitiesDir": "src/entity",
}
}
Typescript config
{
"compileOnSave": false,
"compilerOptions": {
"allowJs": true,
"allowSyntheticDefaultImports": true,
"baseUrl": "src",
"noImplicitAny": false,
"declaration": true,
"emitDecoratorMetadata": true,
"esModuleInterop": true,
"experimentalDecorators": true,
"forceConsistentCasingInFileNames": true,
"importHelpers": true,
"lib": ["es2017", "esnext.asynciterable"],
"module": "commonjs",
"moduleResolution": "node",
"noEmit": false,
"outDir": "./dist",
"pretty": true,
"resolveJsonModule": true,
"sourceMap": true,
"target": "es2017",
"typeRoots": ["node_modules/@types"]
},
"include": ["src/**/*.ts", "src/**/*.json", ".env"],
"exclude": ["node_modules"]
}
Dependencies and package.json
{
"name": "code-deep-dives-tutorial-graphql",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"dependencies": {
"apollo-server": "^2.21.0",
"apollo-server-express": "^3.5.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.13.2",
"graphql": "^15.3.0",
"pg": "^8.7.1",
"reflect-metadata": "^0.1.13",
"ts-node": "^10.4.0",
"tslib": "^2.3.1",
"type-graphql": "^1.1.1",
"typeorm": "^0.2.41",
"typeorm-seeding": "^1.6.1",
"typescript": "^4.5.4"
}
}
Basic server setup
We use TypeGraphQL to create a schema, and then we pass that config into ApolloServer.
This is the basics of it:
import {createConnection, getConnectionOptions} from "typeorm";
import "reflect-metadata";
import {ApolloServer} from "apollo-server";
import {buildSchema} from "type-graphql";
(async function () {
const config = await getConnectionOptions();
await createConnection(config);
const schema = await buildSchema({ // << TypeGraphQL
resolvers: [__dir + '/entities/**/*.ts'],
});
const apolloServer = new ApolloServer({ // << Apollo
schema,
playground: true, // note - this will not work on latest ApolloServer config
});
await apolloServer.listen({port: 6123});
console.log("Apollo server has started on http://localhost:6123!");
})()
Creating a resolver
Simple resolver with a query
A resolver responds to the incoming GraphQL requests (queries or mutations).
They are basically like a controller if you are more used to traditional express apps or MVC apps.
A very basic example of a resolver is below. We have to add the @Resolver
above the class definition, and @Query
above every query.
import {Query, Resolver} from "type-graphql";
@Resolver()
export class ExampleResolver {
@Query(() => String)
sayHello() {
return "hello, world!";
}
}
You use @Query(() => String)
when that function is going to return a string.
Later on you will see it being used with TypeORM entities (objects).
Simple resolver with a mutation
Let's expand on the resolver, and add a mutation.
For now I'm going to just pretend we're speaking to a database. At the moment it will just update a variable message
.
import {Arg, Query, Mutation, Resolver} from "type-graphql";
let message : string | undefined = undefined;
@Resolver()
export class ExampleResolver {
@Query(() => String, {nullable: true})
message() {
if(message === undefined) return null
return `hello, ${message}!`;
}
@Mutation(() => Boolean)
updateMessage(@Arg('newMessage') newMessage: string): bool {
message = newMessage;
return true;
}
}
Now if you do a mutation to updateMessage
, it will update message
variable (until you restart the server), and returns true.
I've also added {nullable: true}
to the query message
. Without this it would not be valid to return null/undefined.
Getting data from the database
Let's set up a TypeORM entity. I'm going to create it based on Twitter, so we could have tweets which are created by a user. For now I'll focus just on the tweets.
In ./src/entity
let's create a 'tweet.ts` file. This is going to be using TypeORM decorators and also TypeGraphQL decorators.
import {
Entity,
PrimaryGeneratedColumn,
Column,
} from "typeorm";
import { ObjectType, Field, ID } from "type-graphql";
@ObjectType() // << type graph ql
@Entity({ name: 'tweets' }) // << type orm
export class Tweet {
@Field( () => ID) << type graph ql
@PrimaryGeneratedColumn() // << type orm
id: number;
@Field(() => String) << type graph ql
@Column({type: "varchar"}) // << type orm
body: string;
}
This entity has just two fields - id
and body
. It has decorators to tell TypeGraphQL that the id
field is a ID
(identifier - like a primary key), and body
is a String
field.
In a real app we would set up dependancy injection, but for this tutorial I'll skip that.
Import getRepository
from TypeORM, and create a new mutation:
import {Arg, Mutation, Resolver} from "type-graphql";
@Resolver()
export class ExampleResolver {
@Mutation(() => Tweet)
async createTweet(@Arg('body') body: string): Promise<Tweet> {
const tweetRepo = getRepository(Tweet);
const newTweet = new Tweet();
newTweet.body = body;
await tweetRepo.save(newTweet);
return newTweet;
}
}
Now if you send a mutation request to createTweet
, it'll save it in the database.
Let's now add a query to get all tweets.
import {Arg, Query, Mutation, Resolver} from "type-graphql";
@Resolver()
export class ExampleResolver {
@Mutation(() => Tweet)
async createTweet(@Arg('body') body: string): Promise<Tweet> {
const tweetRepo = getRepository(Tweet);
const newTweet = new Tweet();
newTweet.body = body;
await tweetRepo.save(newTweet);
return newTweet;
}
@Query(() => [Tweet])
async all(): Promise<Tweet[]> {
const tweetRepo = getRepository(Tweet);
return await tweetRepo.find()
}
}
Using integers and floats in TypeGraphQL
In JS we just have the number
(and BigNumber
) type. They're float numbers, and we don't really have an easy way to define something as only a int or float. So in TypeGraphQL there are aliases to help with this:
// import the aliases
import { ID, Float, Int } from "type-graphql";
@ObjectType()
class MysteryObject {
@Field(type => ID)
readonly id: string;
@Field(type => Int)
emailCount: number;
@Field(type => Float)
popularityScore: number;
}
We still use the typescript number
typing on the class, but we tell TypeGraphQL's Field
function that it is either an Int
or Float
.
You can also see the special ID
type used, which is used to tell graph ql a field is the identifier (like a primary key).
Dates and timestamps in TypeGraphQL
There are a couple of ways to represent dates in TypeGraphQL - either as numbers (timestamp
) or strings (isoDate
).
This can be set when you setup buildSchema()
with config such as:
import { buildSchema } from "type-graphql";
const schema = await buildSchema({
resolvers: [ /* ... */ ],
dateScalarMode: 'timestamp', // "timestamp" or "isoDate"
});
By default they'll come out as isoDate
such as 2021-12-12T12:08:20.221Z
Custom scalar types in TypeGraphQL
Maybe you have a special object that you need to write a custom scalar type for. This is quite straightforward in TypeGraphQL
Example:
import { GraphQLScalarType, Kind } from "graphql";
import { ObjectId } from "mongodb";
export const ObjectIdScalar = new GraphQLScalarType({
name: "ObjectId",
description: "Mongo object id scalar type",
serialize(value: unknown): string {
// check the type of received value
if (!(value instanceof ObjectId)) {
throw new Error("ObjectIdScalar can only serialize ObjectId values");
}
return value.toHexString(); // value sent to the client
},
parseValue(value: unknown): ObjectId {
// check the type of received value
if (typeof value !== "string") {
throw new Error("ObjectIdScalar can only parse string values");
}
return new ObjectId(value); // value from the client input variables
},
parseLiteral(ast): ObjectId {
// check the type of received value
if (ast.kind !== Kind.STRING) {
throw new Error("ObjectIdScalar can only parse string values");
}
return new ObjectId(ast.value); // value from the client query
},
});
import { ObjectIdScalar } from "../ObjectId";
@ObjectType()
class User {
@Field(type => ObjectIdScalar) // << use it here
readonly id: ObjectId; // << and here
@Field()
name: string;
}
If you make heavy use of these, you might want to just import them in your initial server config, and then TypeGraphQL can automatically detect the field type.
import { ObjectId } from "mongodb";
import { ObjectIdScalar } from "../ObjectId";
import { buildSchema } from "type-graphql";
const schema = await buildSchema({
resolvers,
scalarsMap: [{ type: ObjectId, scalar: ObjectIdScalar }],
});
Then you can use it like this:
@ObjectType()
class User {
@Field() // magic goes here - no type annotation for custom scalar
readonly id: ObjectId;
}
Enums
Enums are built into Typescript, example:
// automatic values of 0, 1, 2, 3
enum Direction {
UP,
DOWN,
LEFT,
RIGHT,
}
const dir = Direction.LEFT; // 2
Or you can set the values:
enum Direction {
UP = "up",
DOWN = "down",
LEFT = "left",
RIGHT = "right",
}
const dir = DIRECTION.LEFT; // 'left'
We can also use enums in GraphQL (docs: https://graphql.org/learn/schema/#enumeration-types ), and TypeGraphQL supports it too.
import { registerEnumType } from "type-graphql";
enum Direction {
UP = "UP",
DOWN = "DOWN",
LEFT = "LEFT",
RIGHT = "RIGHT",
}
registerEnumType(Direction, {
name: "Direction",
description: "Direction of travel",
});
Then you can use it in your resolvers like this with the @Arg()
decorator:
@Resolver()
class ExampleResolver {
@Mutation()
move(@Arg("moveTo", type => Direction) moveTo: Direction): string {
moveSomething(moveTo)
return Direction[moveTo];
}
}
function moveSomething(moveTo: Direction) {
// ...
}
Returning multiple types in TypeGraphQL
Let's say you have a search resolver, where you can search for a string and it might return a Movie
or an Actor
.
For this we need to set up a union.
Object type definitions:
@ObjectType()
class Movie {
@Field()
name: string;
@Field()
rating: number;
}
@ObjectType()
class Actor {
@Field()
name: string;
@Field(type => Int)
age: number;
}
And the resolver query method which can return either of those (note: I've not included the findAll
method - lets just pretend we're using TypeORM with active record in those entities)
import {Arg, Query, Resolver, createUnionType } from "type-graphql";
const SearchResultUnion = createUnionType({
name: "SearchResult",
types: () => [Movie, Actor] as const,
});
@Resolver()
class SearchResolver {
@Query(returns => [SearchResultUnion])
async search(@Arg("searchQuery") searchQuery: string): Promise<(typeof SearchResultUnion)[]> {
const movies = await Movies.findAll(searchQuery);
const actors = await Actors.findAll(searchQuery);
return [...movies, ...actors];
}
}
// for easier reading this is not run in Promise.all()
The following shows a more complicated setup of createUnionType
:
const SearchResultUnion = createUnionType({
name: "SearchResult",
types: () => [Movie, Actor] as const,
// our implementation of detecting returned object type
resolveType: value => {
if ("rating" in value) {
return Movie; // we can return object type class (the one with `@ObjectType()`)
}
if ("age" in value) {
return "Actor"; // or the schema name of the type as a string
}
return undefined;
},
});
Then our query can look like this:
query {
search(phrase: "Hello") {
... on Actor {
name
age
}
... on Movie {
name
rating
}
}
}
Using GraphQL interfaces in TypeGraphQL
Although Typescript has interface support, they only exist at compile time. So to use graph ql interfaces we must use abstract classes.
Example:
@InterfaceType()
abstract class IPerson {
@Field(type => ID)
id: string;
@Field()
name: string;
@Field(type => Int)
age: number;
}
And then we can use it like this (the implements: IPerson
is the important part here):
@ObjectType({ implements: IPerson })
class Person implements IPerson {
id: string;
name: string;
age: number;
}
GraphQL directives
GraphQL schema supports directives
, which look like typescript decorators. You can set these up in TypeGraphQL with the @Directive('...')
decorator.
Examples:
@Directive("@auth(requires: USER)")
@ObjectType()
class Foo {
@Field()
field: string;
}
@ObjectType()
class Bar {
@Directive('@deprecated(reason: "Use newField")')
@Field()
field: string;
@Directive("@lowercase")
@Field()
newField: string
}
Once they are set up there, you also need further config to register them. Example:
import { SchemaDirectiveVisitor } from "graphql-tools";
// build the schema as always
const schema = buildSchemaSync({
resolvers: [ExampleResolver],
});
// register the used directives implementations
SchemaDirectiveVisitor.visitSchemaDirectives(schema, {
sample: SampleDirective,
});
How to use dependency injection in TypeGraphQL
It is easy to set up DI. I've used TypeDI in all of these examples as it tends to be the most commonly used DI library paired with TypeGraphQL.
import { buildSchema } from "type-graphql";
import { Container } from "typedi";
import { SampleResolver } from "./resolvers";
const schema = await buildSchema({
resolvers: [SampleResolver],
container: Container, // <<
});
Then your resolvers can use DI like this:
import { Service } from "typedi";
@Service()
@Resolver(() => User)
export class ExampleResolver {
constructor(
private readonly userService: UserService,
) {}
@Query(() => User, { nullable: true })
async recipe(@Arg("userId") userId: string) {
return this.userService.getOne(userId);
}
}
How to add auth to TypeGraphQL
Using the built in @Authorized
decorator, it is quite easy to set up authentication and authorization.
@ObjectType()
class MyObject {
@Field()
publicField: string;
@Authorized()
@Field()
authorizedField: string;
@Authorized("ADMIN")
@Field()
adminField: string;
@Authorized(["ADMIN", "MODERATOR"])
@Field({ nullable: true })
hiddenField?: string;
}
As you can see the @Authorized
decorator has an option param, which can be used to pass to your auth logic (in this case to pass the required role of the user).
You can also add the same logic to your resolvers:
@Resolver()
class MyResolver {
@Query()
publicQuery(): MyObject {
return {
publicField: "Some public data",
authorizedField: "Data for logged users only",
adminField: "Top secret info for admin",
};
}
@Authorized()
@Query()
authedQuery(): string {
return "Authorized users only!";
}
@Authorized("ADMIN", "MODERATOR")
@Mutation()
adminMutation(): string {
return "You are an admin/moderator, you can safely drop the database ;)";
}
}
Now we need to set up the logic to allow to deny access to those protected by @Authorized
. The most simple way is with a function:
export const customAuthChecker: AuthChecker<ContextType> = (
{ root, args, context, info },
roles,
) => {
// add your logic to return true/false depending if the current user
// has any of `roles`. E.g. look at session cookie, JWT token, etc.
return true;
};
const schema = await buildSchema({
resolvers: [MyResolver],
authChecker: customAuthChecker, // << the important part
});
How to get access to the request (context) in ApolloServer
import {createConnection, getConnectionOptions} from "typeorm";
import "reflect-metadata";
import {ApolloServer} from "apollo-server";
import {buildSchema} from "type-graphql";
(async function () {
const config = await getConnectionOptions();
await createConnection(config);
const schema = await buildSchema({
resolvers: [__dir + '/entities/**/*.ts'],
});
const apolloServer = new ApolloServer({ // << Apollo
schema,
playground: true,
// Important bit:
context: ({ req }) => {
const context = {
req,
};
return context;
},
});
await apolloServer.listen({port: 6123});
console.log("Apollo server has started on http://localhost:6123!");
})()
Then you can access the context's req
object
@Resolver()
class ExampleResolver {
@Query()
showMessage(@Ctx() ctx: Context): string {
const req = ctx.req;
return 'hello, world';
}
}
Adding validation
It is easy to add validation. Juse use the @InputType()
along with validators from class-validator
package:
import { MaxLength, Length } from "class-validator";
@InputType()
export class RecipeInput {
@Field()
@MaxLength(30)
title: string;
@Field({ nullable: true })
@Length(30, 255)
description?: string;
}
Note: we do not need to add @IsString
etc - as TypeGraphQL automatically checks those based on the typing. Nested inputs or arrays need special attention though (with @ValidateNested
).
@Resolver(of => Recipe)
export class RecipeResolver {
@Mutation(returns => Recipe)
async addRecipe(@Arg("input") recipeInput: RecipeInput): Promise<Recipe> {
// you can be 100% sure that the input is correct
console.assert(recipeInput.title.length <= 30);
console.assert(recipeInput.description.length >= 30);
console.assert(recipeInput.description.length <= 255);
}
}
Automatic validation in TypeGraphQL is enabled by default, but can be disabled:
const schema = await buildSchema({
resolvers: [ExampleResolver],
validate: false,
});
and then enable it on specific cases:
class ExampleResolver {
@Mutation(() => Recipe)
async addRecipe(@Arg("input", { validate: true }) recipeInput: RecipeInput) {
// ...
}
}