Return to blog

Implementing server-side CRUD with TypeScript, TypeORM and GraphQL

hero image for Implementing server-side CRUD with TypeScript, TypeORM and GraphQL

Introduction

Today, I would like to explain how to use GraphQL with TypeScript. GraphQL is very interesting technology invented by Facebook in 2012 and presented to the audience in 2015. It's used for getting specific data by the client.

Before getting to the GraphQL server implementation, let's get familiar with some theory.

What is GraphQL? It's a query language for APIs. This technology introduces a flexible way to get the data.

What does it mean? Imagine that you have a big amount of data, e.g, personal data. Your only task is to get the name and ID of one single user. With GraphQL, you can do it easily because the query contains needed fields only.

There's an alternative to GraphQL – REST. So why using GraphQL at all? The best way to answer this question is to compare these two technologies. Let's start with creating queries (GraphQL) and routes (REST):

REST
/users/<id>
/users/<id>/posts
/users/<id>/followers

GraphQL
query {
	User(id: <id>) {
		name
		posts {
			title
		}
	}
}

At first glance, GraphQL might seem a bit more complicated, but only for a moment. This query tells us that client takes the User object with given ID and extracts the user’s name and post titles.

The main differences between GraphQL and REST are:

  • GraphQL eliminates the well-known REST problem: underfetching and overfetching
  • GraphQL allows for rapid product iteration on the frontend
  • GraphQL uses SDL (Schema Definition Language) which serves the contract on the line client-server to define the client's data access.

There is one more difference. In REST, we are defining a lot of routes, depending on what we need. GraphQL provides us just with one route /graphql. The name of the route can be modified.

Both GraphQL and REST have their own destinations. However, it's possible to use both solutions in one project. For example, GraphQL can be used as internal API between services to extract specific data only, while REST can integrate third party APIs.

Now, we're rady to jump right to the code! We're going to create a simple server side CRUD application to work on the user objects.

Requirements

  • TypeScript 3.8.3
  • NodeJS 12.x.x
  • Your favorite code editor (I highly recommend Visual Studio Code)

Creating project

Let’s start with creating our project. I use the terminal to create a new node app. Use npm init command to set up your application. The output should generate package.json witha content similar to the following:

{
  "name": "graphql-typescript-demo",
  "version": "1.0.0",
  "description": "Demo application, simple CRUD uses graphql and typescript",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/DominikMarciniszyn/graphql-typescript-demo.git"
  },
  "keywords": [
    "graphql",
    "typescript",
    "nodejs",
    "typeorm"
  ],
  "author": "Rouch",
  "license": "ISC",
  "bugs": {
    "url": "https://github.com/DominikMarciniszyn/graphql-typescript-demo/issues"
  },
  "homepage": "https://github.com/DominikMarciniszyn/graphql-typescript-demo#readme"
}

We have to add dependencies that we're going need for package.json. Add the code written below at the end of your file.

"devDependencies": {
  "nodemon": "2.0.3",
  "ts-node": "8.9.1",
  "typescript": "3.8.3"
}

Nodemon is a tool which helps us with development. It automatically restarts the application when any change occurs. To use nodemon we have to define start command in the scripts section. The ts-node dependency allows to run TypeScript seamlessly in Node.js.

"scripts": {
  "start": "nodemon -w src --ext ts --exec ts-node backend/src/index.ts"
}

We also need to add production dependencies to the package.json file.

"dependencies": {
  "apollo-server": "2.12.0",
  "graphql": "14.1.1",
  "reflect-metadata": "0.1.13",
  "sqlite3": "4.2.0",
  "type-graphql": "0.17.6",
  "typeorm": "0.2.24",
  "typeorm-encrypted": "0.5.4"
}

A short description of the dependencies named above:

  • Apollo server is used to build and run the GraphQL server
  • Reflect-metadata allows to us to use TypeScript decorators
  • SQLite will be our database
  • Type GraphQL helps us create schema from model classes
  • TypeORM enables the interaction with our SQLite database
  • TypeORM Encrypted enables the use of encryption in the password field

Now, we can install all dependencies using npm install command.

The next step will be configuration of TypeScript. We have to create tsconfig.json file to proceed.

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "CommonJS",
    "strict": true,
    "esModuleInterop": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "strictPropertyInitialization": false
  }
}

Make sure that experimentalDecorators and emitDecoratorMetadata are set to true.

Setting up project structure

Let’s prepare the project structure. If you prefer a different type of structure, feel free to use your favorite one. Here's my proposal:

In the picture, we can see four key elements:

  • Inputs this folder contains project’s logic - defines what user can put into objects
  • Models – keeps database entities
  • Resolvers  keeps resolver functions
  • index.ts is the entry point of our application

I'll explain what resolvers are later during the implementation.

Setting up database

There are several approaches to define database configuration in TypeORM. I prefer to create configuration by simple json config. Let’s create ormconfig.json file. TypeORM supports many databases, including popular ones like PostgreSQL and MySQL. You can use whatever database you want, but for simplicity's sake, I’m going to use SQLite. This is a very handy database to write demos or prototypes. Add the ormconfig.json at the same level as package.json.

{
  "type": "sqlite",
  "database": "./db.sqlite3",
  "entities": ["./backend/src/models/*.ts"],
  "synchronize": true
}

Models

Model is a class that allows to interact with specific table in the database. TypeORM allows us to define model using decorators. Let’s create user model.

// backend/src/models/user.ts

import { Entity, BaseEntity, PrimaryGeneratedColumn, Column } from 'typeorm';
import { EncryptionTransformer } from 'typeorm-encrypted';


@Entity()
export class User extends BaseEntity {
  @PrimaryGeneratedColumn()
  id: string;

  @Column()
  firstname: string;

  @Column()
  lastname: string;

  @Column()
  nickname: string;

  @Column()
  email: string;

  @Column({transformer: new EncryptionTransformer({
    key: 'e41c966f21f9e1577802463f8924e6a3fe3e9751f201304213b2f845d8841d61',
    algorithm: 'aes-256-cbc',
    ivLength: 16,
    iv: 'ff5ac19190424b1d88f9419ef949ae56'
  })})
  password: string;
}

The TypeORM model is a TypeScript class wrapped with Entity decorator. The BaseEntity class contains a lot of useful methods to access our user table. It's worth to pay attention to the transformer used in the password field. Transformer works in two ways. Every time the user passes the password in a mutation (later on, I’ll explain what it is), the password field will be encrypted with given algorithm. When user wants to get the password the transformer decrypts it. I did it for tutorial purposes, to show all the details of how it works.

Since we're building GraphQL API, we also need to define object types. We can do it the same way as database entity. We're going to use decorators again. We can combine database entity with type GraphQL decorators. As a result, we get a class which represents database model and GraphQL object. The code should look like this:

// backend/src/models/user.ts

import { Entity, BaseEntity, PrimaryGeneratedColumn, Column } from 'typeorm';
import { ObjectType, Field, ID } from 'type-graphql';
import { EncryptionTransformer } from 'typeorm-encrypted';


@ObjectType()
@Entity()
export class User extends BaseEntity {
  @Field(() => ID)
  @PrimaryGeneratedColumn()
  id: string;

  @Field(() => String)
  @Column()
  firstname: string;

  @Field(() => String)
  @Column()
  lastname: string;

  @Field(() => String)
  @Column()
  nickname: string;

  @Field(() => String)
  @Column()
  email: string;

  @Field(() => String)
  @Column({transformer: new EncryptionTransformer({
    key: 'e41c966f21f9e1577802463f8924e6a3fe3e9751f201304213b2f845d8841d61',
    algorithm: 'aes-256-cbc',
    ivLength: 16,
    iv: 'ff5ac19190424b1d88f9419ef949ae56'
  })})
  password: string;
}

It's a very efficient approach. We define single data type, containing database model and GraphQL object, in one place. It reduces errors caused by property inconsistencies. Let’s say we want to change the field nickname to nick. In standard GraphQL approach, we need to define this field in schema and database model. Thanks to the use of decorators we can reduce the modification time by editing just the property name in our class.

Resolvers

It's time for an extremely significant chapter of this tutorial. I’m going to start with the explanation of what resolver methods and basic GraphQL operations are. To build our resolver functions, the first thing that we need to know is what queries, mutations and subscriptions are.

  • Query - is like a GET in REST. By query, you ask the server for specific fields in object.
  • Mutation - is like operations in REST which modifies data (POST, PUT, PATCH). By mutation, you can, i.a., create a new object or update an existing one.
  • Subscription - subscriptions are a way to push data from a server to the clients that choose to listen to real time messages from the server.

Now, let’s go back to resolvers. The resolvers usually are collections of functions that are mapped into a single object that has to match with the earlier defined schema. It looks quite complicated because we need to define both schema and resolver in separate places.

With TypeGraphQL, we don’t need to write the schema explicitly. Instead, we can define resolvers with the decorators and the library to generate the schema for us.

Later on, we will define CRUD operations, but we can define simple hello world resolver right now!

// backend/src/resolvers/user_resolver.ts

import { Resolver, Query } from "type-graphql";


@Resolver()
export class UserResolver {

  @Query(() => String)
  hello() {
    return "world";
  }
}

The UserResolver class is decorated by Resolver. This allows us to keep all our resolvers connected with User in one place. We want to make sure we decorate the method with either Query or Mutation and pass the return type as the first parameter. So far, we have the hello query, but how can we use it now? We need to go back to index.ts and implement our entry point.

Setting up the server

It’s time to create the entry point!

// backend/src/index.ts

import 'reflect-metadata';
import { createConnection } from 'typeorm';
import { ApolloServer } from 'apollo-server';
import { buildSchema } from 'type-graphql';
import { UserResolver } from './resolvers/user_resolver';


async function runServer() {
  const connection = await createConnection();
  const schema = await buildSchema({
    resolvers: [UserResolver]
  });

  const server = new ApolloServer({ schema });
  await server.listen(8050);

  console.log('Server started at port ::8050');
}


runServer();

In this file, we can write a function named runServer. This function is an entry point of our server application. Inside this method, we are creating connection with the database. Next, we are generating GraphQL schema using buildSchema method. As an argument, we are passing object with list of our resolvers. The reflect-metadata package that we imported at the top of the file is a helper library that extends the functionality of TypeScript decorators. This package is required to use TypeORM and TypeGraphQL. Finally, we're initialising apollo server which is listening into the 8050 port (You can use other port if you want).

We're reaty to start our server by using npm start command.

Navigate to 127.0.0.1:8050. You should see GraphQL Playground where we can test our hello resolver now.

Oh yes! It works! Now, we can create our CRUD application.

Database CRUD

If our server works, we can implement CRUD operations. Let’s start with getting all users.

import { Resolver, Query } from "type-graphql";
import { User } from '../models/user';


@Resolver()
export class UserResolver {

  @Query(() => [User])
  users() {
    return User.find();
  }
}

We created users method inside our resolver class and decorated it with Query. In decorator arguments, we defined return type. In this case it's an array of users. Inside users method, we are using find method provided by our model. Let’s go back to GraphQL Playground and see how the users query works.

It returns an empty array which means that we don’t have any records inside the database. Let’s add the mutation to create users.

@Mutation(() => User)
async createUser(@Arg("data") data: CreateUserInput) {
  const user = User.create(data);
  await user.save();
  return user;
}

We created createUser method that returns a User type. Inside this method, we create a user object with given data, save it to the database, and return it. This method requires data as a parameter. We can build an input type to specify what fields are necessary for this mutation.

// backend/src/inputs/create_user_input.ts

import { InputType, Field } from 'type-graphql';


@InputType()
export class CreateUserInput {
  @Field()
  firstname: string;

  @Field()
  lastname: string;

  @Field()
  nickname: string;

  @Field()
  email: string;

  @Field()
  password: string;
}

Input class is similar to our object type. We decorate the class with InputType. As you can see, there is no ID field in input class. It’s correct because ID is autogenerated by the database. Now let’s try this mutation.

The mutation returns password as a plain text, but it's encrypted inside the database. It's how the transformer in our model works.

Afterwards, we're going to create a function to get user by id.

@Query(() => User)
user(@Arg("id") id: string) {
  return User.findOne({ where: { id }});
}

And the check in the GraphQL Playground:

At this point, we're going to create an update method:

@Mutation(() => User)
async updateUser(@Arg("id") id: string, @Arg("data") data: UpdateUserInput) {
  const user = await User.findOne({ where: { id }});

  if (!user) {
    throw new Error(`The user with id: ${id} does not exist!`);
  }

  Object.assign(user, data);
  await user.save();

  return user;
}

In the updateUser, we need the user’s id as well as input data. First, we check if a user with given id exists in the database. Then we update properties of this user using data parameter. At the end, we save the changes to the database and return updated user object.

// backend/src/inputs/update_user_input.ts

import { InputType, Field } from "type-graphql";


@InputType()
export class UpdateUserInput {
  @Field({ nullable: true })
  firstname?: string;

  @Field({ nullable: true })
  lastname?: string;

  @Field({ nullable: true })
  nickname?: string;

  @Field({ nullable: true })
  email?: string;

  @Field({ nullable: true })
  password?: string;
}

Fields in input class are optional since we may want to update only one field.

Let’s check what happens when we want to update a user that doesn't exist.

As you can see, we received an error with message exactly defined in our updateUser resolver. Now, the last operation  deleteUser.

@Mutation(() => Boolean)
async deleteUser(@Arg("id") id: string) {
  const user = await User.findOne({ where: { id }});

  if (!user) {
    throw new Error(`The user with id: ${id} does not exist!`);
  }

  await user.remove();
  return true;
}

This resolver method is very simple. We pass the id of the user we want to remove. Then we check if such a user exists in the database. If so, the resolver will remove the user object and turn back to true once the operation is done.

Summary

The combination of TypeORM and TypeGraphQL is unbelievably effective and speeds up development. It also prevents us from repeating the same tasks over and over again. The code is simple to read and easy to extend. These tools are worth consideration for creating future projects.

Link to repository: https://github.com/DominikMarciniszyn/graphql-typescript-demo

Useful materials:

As a reliable software company we’re focused on delivering the best quality IT services. However, we’ve discovered that programming skills give us a very particular opportunity...

.eco profile for codetain.eco

Reach Us

65-392 Zielona Góra, Poland

Botaniczna 70

© 2015-2024 Codetain. All rights reserved.

cnlogo