Editor’s note: This post was revised on 15 December 2021 to include updated code blocks and information.
GraphQL’s popularity is constantly growing, and it’s no mystery as to why: it’s a great tool that solves many common problems developers encounter with RESTful APIs. GraphQL allows us to easily fetch data relations, but it also prevents us from over-fetching that data. Put simply, GraphQL improves the development experience and makes frontend apps faster.
If you have ever created a GraphQL server with Node.js before, chances are, you define the schema for your object types, mutations, and queries in GraphQL schema language and the resolvers in JavaScript/TypeScript, which match the schema you defined.
This approach can be hard to maintain, especially when dealing with a large schema because your schema and resolvers are in separate places.
If we want to change a field in our data, we need to change the database model class and GraphQL schema and adjust the type interface (if using TypeScript). But in this tutorial, I’m going to show you an enjoyable way to build a GraphQL API with TypeGraphQL and TypeORM.
TypeGraphQL is a framework for building GraphQL APIs with Node.js and TypeScript. The main purpose of this tool is to let us define our schema directly from our TypeScript code. TypeORM, on the other hand, is a TypeScript library that allows us to interact with SQL databases. With these tools combined, we can build a type-safe GraphQL API without the frustrations that usually come with such a project.
Without further ado, let’s walk through how to build a GraphQL API with TypeGraphQL and TypeORM that can manage books data with CRUD functionalities.
Before you get started, make sure that you:
We’ll begin by initializing a new Node.js project.
mkdir learn-typegraphql npm init -y
Next, we’ll install some dependencies.
npm install apollo-server type-graphql typeorm reflect-metadata
Here, we are installing:
reflect-metadata
to work with TypeScript decoratorsIn addition, we need to install some development dependencies.
npm install -D typescript ts-node nodemon
This script will install:
ts-node
to run our server in a development environmentnodemon
to automatically restart the server whenever we make changes to the codeNow, to make our job a bit easier, let’s define the npm start scripts in package.json
.
{ // ... "scripts": { "start": "nodemon -w src --ext ts --exec ts-node src/index.ts" } }
Next, create a tsconfig.json
file. This file contains our TypeScript configurations because we will use some TypeScript features that are currently still experimental, yet stable enough for our purposes.
{ "compilerOptions": { "target": "es5", "module": "commonjs", "strict": true, "esModuleInterop": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, "strictPropertyInitialization": false } }
Make sure the experimentalDecorators
and emitDecoratorMetadata
are set to true.
For reference, I published the entire source code of this project to my GitHub. Feel free to poke around or clone it onto your computer.
git clone https://github.com/rahmanfadhil/learn-typegraphql.git
It’s time to start working on our server API. Let’s create a new file called index.ts
inside the src
folder.
// src/index.ts import "reflect-metadata"; import { createConnection } from "typeorm"; import { ApolloServer } from "apollo-server"; import { buildSchema } from "type-graphql"; async function main() { const connection = await createConnection() const schema = await buildSchema() const server = new ApolloServer({ schema }) await server.listen(4000) console.log("Server has started!") } main()
In this file, we can write a function called main
. This function makes it easier to initialize every single library that we use in this project. In this function, we’ll first create a new connection to our database with the createConnection
function provided by TypeORM.
Next, we’ll generate our GraphQL schema with the buildSchema
method by TypeGraphQL. This will take all our resolvers and generate an executable GraphQL schema we can use inside our Apollo Server. These resolvers are a little bit different, which we’ll discuss later in this tutorial.
The reflect-metadata package we imported at the top is a helper library that extends the functionality of TypeScript decorators. This package is required to use TypeORM and TypeGraphQL.
Finally, we’ll initialize our Apollo Server, pass our schema, and start it in port 4000
(or any other port you want).
In the previous section, we call the createConnection
function from TypeORM to establish a database connection. Now, we need to define a database configuration to tell TypeORM which kind of database we plan to use and how to access it. There are several ways to do this; personally, I like to create the configuration inside the ormconfig.json
file.
Currently, TypeORM supports nine types of SQL databases, including popular ones such as MySQL and PostgreSQL. You can use any database you want, but for the sake of simplicity, I’m going to use SQLite — the smallest implementation of SQL database engine that is very easy to get started. To use this database, we must first install the driver for Node.js.
npm install sqlite3
Now, we can add the ormconfig.json
file to our project.
{ "type": "sqlite", "database": "./db.sqlite3", "entities": ["./src/models/*.ts"], "synchronize": true }
Suppose you create a GraphQL server with just the Apollo library. In that case, you define the schema for your object types, mutations, and queries in GraphQL schema language and the resolvers in JavaScript/TypeScript that match the schema you defined. This approach can be tough to maintain, especially when you have a lot of mutations and queries because your schema and resolvers are in separate places.
With TypeGraphQL, however, we don’t need to explicitly write the schema. Instead, we define our resolvers with TypeScript classes and decorators, and TypeGraphQL will generate the schema for us.
To see this in action, let’s take a look at the code below.
// src/resolvers/BookResolver.ts import { Resolver, Query } from "type-graphql"; @Resolver() export class BookResolver { @Query(() => String) hello() { return "world"; } }
Here, we created a class called BookResolver
and decorated it with the Resolver
decorator by TypeGraphQL. This enables us to place all our resolvers in this class as a method. We also want to make sure we decorate the method with either Query
or Mutation
and pass the return type on the first parameter.
So far, we just have a hello
query that will return a string. Later, we will implement full CRUD to our database with TypeORM.
Now we need to register our resolver in src/index.ts
.
import "reflect-metadata"; import { createConnection } from "typeorm"; import { ApolloServer } from "apollo-server"; import { BookResolver } from "./resolvers/BookResolver.ts"; // add this import { buildSchema } from "type-graphql"; async function main() { const connection = await createConnection() const schema = await buildSchema({ resolvers: [BookResolver] // add this }) const server = new ApolloServer({ schema }) await server.listen(4000) console.log("Server has started!") } main()
That’s it! To make sure everything is set up properly, let’s try to run our server by running npm start
on the terminal and opening localhost:4000
in the browser.
Now that our server is up and running, the next step is to define our models.
A model is essentially a class that allows us to interact with a specific table in our database. With TypeORM, we can define our database models with classes and decorators, just like our resolvers. And because we’re trying to build a bookstore API, let’s create a model that represents our books.
// src/models/Book.ts import { Entity, BaseEntity, PrimaryGeneratedColumn, Column } from "typeorm"; @Entity() export class Book extends BaseEntity { @PrimaryGeneratedColumn() id: string; @Column() title: string; @Column() author: string; @Column({ default: false }) isPublished: boolean; }
A TypeORM model is essentially a plain TypeScript class that is decorated with Entity
. This class contains properties that represent the fields of our table in the database. You can read more about it in the TypeORM official documentation.
This class extends the BaseEntity
class, which contains useful methods to access our books table.
Because we are building a GraphQL API, we also need to define our object types. In GraphQL, every query and mutation returns an object, whether it’s a boolean, string, or a custom object we define ourselves. Just like our models, we can simply define our object types by using classes and decorators.
Here is where the magic happens. We can combine both TypeGraphQL and TypeORM decorators in a single TypeScript class. In that way, we can have a class that represents both GraphQL object types, as well as the database model. The code should look something like this:
// src/models/Book.ts import { Entity, BaseEntity, PrimaryGeneratedColumn, Column } from "typeorm"; import { ObjectType, Field, ID } from "type-graphql"; @Entity() @ObjectType() export class Book extends BaseEntity { @Field(() => ID) @PrimaryGeneratedColumn() id: string; @Field(() => String) @Column() title: string; @Field(() => String) @Column() author: string; @Field(() => Boolean) @Column({ default: false }) isPublished: boolean; }
This makes our code a lot more efficient because we define a single data type in one place, which should help reduce errors caused by property inconsistency.
Let’s say we want to update the isPublished
property to published
. Traditionally, when using the default GraphQL schema language, we’d need to define our data type in both the database model and the GraphQL schema. However, by using these decorators, we can simply update the property in our class to update both the schema and the model.
You can even have different names for your database table column and your GraphQL schema. Suppose you want to name the isPublished
field to is_published
in the database and published in the GraphQL schema, you can do so by adding the name option to the property decorator:
@Field(() => Boolean, { name: "published" }) @Column({ default: false, name: 'is_published' }) isPublished: boolean;
After we create our database models, let’s go back to our resolvers and implement a query that returns all our books.
import { Resolver, Query } from "type-graphql"; import { Book } from "../models/Book"; @Resolver() class BookResolver { @Query(() => [Book]) books() { return Book.find() } }
We’ll create the books
method inside our resolver class and decorate it with Query
. To specify the return type of our query, we need to pass it inside the Query
decorator argument, which, in this case, is an array of books. Inside this method, we fetch our book with the find
method from our model.
Now let’s go back to our playground and test this query.
It returns an empty array, which means we’ve yet to create any books. Let’s do so by creating a mutation.
@Mutation(() => Book) async createBook(@Arg("data") data: CreateBookInput) { const book = Book.create(data); await book.save(); return book; }
Here, we’re creating a createBook
method that will return a book type. In this method, we initialize a new instance of Book
, save it to the database with the save
method, and return it. This method requires data as a parameter. To obtain data from users, we can build an input type to specify what fields are necessary for this mutation.
Let’s create an input to create a new book.
// src/inputs/CreateBookInput.ts import { InputType, Field } from "type-graphql"; @InputType() export class CreateBookInput { @Field() title: string; @Field() author: string; @Field({ nullable: true }) isPublished?: boolean; }
The input type class is similar to our object type. The only difference is that we decorated the class with InputType
. Here, we specify all the columns except the id
column because it is auto-generated by the database every time we insert a new row. We also set nullable: true
for the isPublished
column because it has a default value.
Let’s test it out!
Next, we’ll create a new query to fetch an individual book.
@Query(() => Book) book(@Arg("id") id: string) { return Book.findOne({ where: { id } }); }
Let’s try this query:
So far, so good!
Now it’s time to add the update operation.
@Mutation(() => Book) async updateBook(@Arg("id") id: string, @Arg("data") data: UpdateBookInput) { const book = await Book.findOne({ where: { id } }); if (!book) throw new Error("Book not found!"); Object.assign(book, data); await book.save(); return book; }
In the updateBook
method, we need the id
of the book
we want to update as well as the user input, which we will create later. First, we’ll find the book, if it exists. Then, we’ll update the properties defined in the data
parameter. Finally, we’ll save all changes to the database and return the updated book data to the user.
Below we define the input for updating a book.
import { InputType, Field } from "type-graphql"; @InputType() export class UpdateBookInput { @Field({ nullable: true }) title?: string; @Field({ nullable: true }) author?: string; @Field({ nullable: true }) isPublished?: boolean; }
The input is very similar to our CreateBookInput
class. However, all these properties are optional, which means the user doesn’t have to fill all the properties of the book.
The last step is to implement the delete book feature.
@Mutation(() => Boolean) async deleteBook(@Arg("id") id: string) { const book = await Book.findOne({ where: { id } }); if (!book) throw new Error("Book not found!"); await book.remove(); return true; }
The method is pretty straightforward. We find that book from the given id
, remove it from the database with remove
function, and return true
for the result.
TypeGraphQL can help solve many of the issues developers encounter when building GraphQL APIs, especially with TypeScript. Not only does it offer a cleaner and safer way to build GraphQL APIs, but it also prevents us from repeating the same tasks over and over.
It’s even more useful if you use TypeORM because it takes the same approach to define your database models. These tools are proven to get the job done and should be a top consideration for your next big project.
LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your site. Instead of guessing why problems happen, you can aggregate and report on problematic GraphQL requests to quickly understand the root cause. In addition, you can track Apollo client state and inspect GraphQL queries' key-value pairs.
LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free.Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.
16 Replies to "Building GraphQL APIs with TypeGraphQL and TypeORM"
Any chance you could cover (or update this) to also show how to use typeorm and type-graphql to create an interface (i.e. interface User types teacher / student)?
Great walkthrough! I had a suggestion for the index.ts that is missing an import for type-graphql to use buildSchema()
Good article. Looks extremely promising as a way to use GraphQL with TypeORM. How does this work with migrations? And, have you created any type of system using this approach with a large number of tables with relationships between them? I’m wondering how this will perform when there are 400+ tables.
Thanks!
If you’re dealing with a large number of tables, I suggest you take a look at DataLoader. You can use it alongside with TypeGraphQL and TypeORM. It allows you to gather every table relation query that happens in a single request and run them at the same time.
I cannot for the life of me get this to build in Typescript. Dev works fine, but trying to run a built output always comes back with `import { Entity, BaseEntity, PrimaryGeneratedColumn, Column } from “typeorm”;`
I’m getting this error Property ‘save’ does not exist on type ‘Book[]’.ts(2339)
Thank you for this article.
I like the simplicity of typeorm + type-graphql for basic CRUD operation.
But it’s difficult to find real life examples with relationships (one to many, many to one, many to many)
Ben Awad released a video, with data loader, but honestly it’s seems to be a pain in the a**
Hi Jerome, you find it difficult to find examples of one to many, many to one and many to may – well here’s a few for you:
Customer *—————–* Product (Customer buys many Products – Product Type really – and a Product Type can be bought by many customers).
Developer *———————–*Programming Languages
Developer may use many PLs, a PL may be used by many individual Developers.
One to Many
Mother 1 ———————— * Child
A mother may have many children (0..n), a Child will have exactly one Mother.
And so on.
Perhaps it’s the meaning of Many to Many, One to Many that is confusing?
Very nice article. I like the conventions of a single model/entity with the decorators for both TypeORM and GraphQL. The more conventional and repeatable the mechanism – the more it becomes a candidate for code generation and/or scaffolding via a CLI (i.e., Angular Schematics).
I’m just getting started with this technology stack – however, the maturity of tools and process make it a strong candidate for enterprise applications. Ping me on twitter @Angularlicious, I’d love to continue the conversation…on my podcast, perhaps? Let me know. Thanks!
Create an issue on Github (with full error logs, etc…), maybe someone can help.
Nice hint to combine Entities and type-graphql fields and type-orm entities. However I can see potential issues that could enforce a separation between the two type of objects. What if you have different database schema than what you want GQL queries structure? What if the database fields change but you don’t want to break the GQL interface?
Thank you so much!
Hi , very clear article , help me a lot . i have a one question though , we are extending books by BaseEntity and when creatingBook if we try to declare return type as
createCategory(
@Arg(‘category’) category: AddCategoryInput
):Promise { ..
then it return error “missing properties hasId , save , remove etc” which are BaseEntity properties . any suggestions to handle. thanks in advance.
Wow! That’s the more useful post I’ve find about GraphQL API so far! Thank you, that was perfetc!
@John … to create Types, you’ll use the ObjectType decorator .. like this
@ObjectType({ description: “The recipe model” })
class Recipe {
@Field(type => ID)
id: string;
@Field({ description: “The title of the recipe” })
title: string;
@Field(type => [Rate])
ratings: Rate[];
@Field({ nullable: true })
averageRating?: number;
}
Wow, this tutorials is amazing. Great Job!