I was building a GraphQL API recently, and a familiar frustration crept in. I had my TypeORM entities neatly defined for the database. Then, I had to create nearly identical GraphQL types for my schema. When I changed a field in the database model, I’d forget to update the GraphQL type. The compiler wouldn’t catch it, and bugs would slip through. It felt like maintaining two separate, yet identical, blueprints. There had to be a better way to keep everything in sync.
That’s when I started combining TypeGraphQL with TypeORM. The idea is simple but powerful: use one TypeScript class for both your database table and your GraphQL type. You decorate it once, and it works everywhere. This approach gives you end-to-end type safety, from the database query all the way to the API response. The compiler becomes your strictest code reviewer, catching mismatches before they become runtime errors.
Let’s see what this looks like in practice. Instead of writing two separate definitions, you write one.
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";
import { ObjectType, Field, ID } from "type-graphql";
@ObjectType()
@Entity()
export class User {
@Field(() => ID)
@PrimaryGeneratedColumn()
id: number;
@Field()
@Column()
email: string;
@Field()
@Column()
name: string;
}
Notice the decorators from both libraries on the same class. @Entity and @Column tell TypeORM this is a database table. @ObjectType and @Field tell TypeGraphQL this is a GraphQL type. This single User class is now the single source of truth. Change the name column type here, and both your database migrations and your GraphQL schema reflect that change instantly.
But what about relationships? This is where the integration truly shines. Defining how entities connect is just as straightforward.
import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from "typeorm";
import { ObjectType, Field, ID } from "type-graphql";
import { Post } from "./Post";
@ObjectType()
@Entity()
export class User {
@Field(() => ID)
@PrimaryGeneratedColumn()
id: number;
@Field()
@Column()
email: string;
@Field(() => [Post])
@OneToMany(() => Post, (post) => post.author)
posts: Post[];
}
Here, the @OneToMany decorator from TypeORM sets up the database relationship. The @Field(() => [Post]) decorator from TypeGraphQL exposes this list of posts in the GraphQL schema. The types are shared, so you know the posts field will always be an array of the Post entity. Have you ever had a bug where a relationship field returned the wrong shape of data? This setup makes those mistakes nearly impossible.
Of course, a GraphQL API needs resolvers to fetch data. With this unified model, creating a resolver is clean and type-safe. You work directly with the TypeORM repository, and the return types are your GraphQL types.
import { Resolver, Query, Arg } from "type-graphql";
import { User } from "../entity/User";
import { AppDataSource } from "../data-source"; // Your TypeORM DataSource
@Resolver()
export class UserResolver {
private userRepository = AppDataSource.getRepository(User);
@Query(() => User, { nullable: true })
async user(@Arg("id") id: number): Promise<User | null> {
return this.userRepository.findOneBy({ id });
}
@Query(() => [User])
async users(): Promise<User[]> {
return this.userRepository.find();
}
}
The user query is declared to return the User GraphQL type. The findOneBy method from TypeORM also returns a promise of a User entity. Because they are the same class, the types match perfectly. The compiler will complain if you try to return anything else. This removes the mental load of translating between different object shapes.
You might be wondering about more complex scenarios. Let’s say you want to create a new user through a GraphQL mutation. You can create an Input Type, which is just another class with TypeGraphQL decorators. Often, this input type can reuse the validation logic you might already have on your entity.
import { InputType, Field } from "type-graphql";
import { IsEmail } from "class-validator";
@InputType()
export class CreateUserInput {
@Field()
@IsEmail()
email: string;
@Field()
name: string;
}
Then, in your resolver, you can use this input type and pass its data directly to TypeORM’s create and save methods.
@Mutation(() => User)
async createUser(@Arg("data") data: CreateUserInput): Promise<User> {
const user = this.userRepository.create(data); // TypeORM creates an instance
return this.userRepository.save(user); // TypeORM saves it to the DB
}
The flow is consistent and safe. The GraphQL input is validated, transformed into a TypeORM entity instance, and saved. The returned value is again the User entity/type. This end-to-end type coverage is what prevents bugs. It turns runtime schema mismatches into compile-time errors.
The benefits go beyond just preventing bugs. Developer experience improves dramatically. You spend less time writing boilerplate and synchronizing files. Refactoring becomes a confident process. You can rename a field or change its type, and your IDE’s refactoring tools will update all references across your resolvers, queries, and database logic. The compiler guides you through the changes.
This pattern is incredibly useful for applications where data integrity is non-negotiable. Think of an admin panel where a Product entity has connections to Inventory, PriceHistory, and Category tables. Using integrated TypeORM and TypeGraphQL classes ensures that the complex object graph you fetch from the database is exactly the one you expose through your API, with full type support at every step.
Getting started is simple. After setting up TypeORM and TypeGraphQL individually, you begin defining your entities with both sets of decorators. Start with a core model, like a User or Product, and build your resolvers around it. You’ll quickly appreciate how much less code you write and how much more confidence you have in it.
I found that this integration completely changed how I build backends. The constant context-switching between database shapes and API shapes disappeared. The code became more declarative and easier to reason about. If you’re tired of managing parallel type definitions and want your compiler to do the heavy lifting, give this combination a try.
What was the last bug you had that was caused by a type mismatch between layers? Could this approach have caught it? I’d love to hear about your experiences. If this way of building type-safe APIs resonates with you, please share this article with a teammate or leave a comment below. Let’s discuss how we can write more robust software, together.
As a best-selling author, I invite you to explore my books on Amazon. Don’t forget to follow me on Medium and show your support. Thank you! Your support means the world!
101 Books
101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.
Check out our book Golang Clean Code available on Amazon.
Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!
📘 Checkout my latest ebook for free on my channel!
Be sure to like, share, comment, and subscribe to the channel!
Our Creations
Be sure to check out our creations:
Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | JS Schools
We are on Medium
Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva