I’ve been thinking about this problem for weeks. Every time I build a GraphQL API, I find myself writing the same things three times: once for the database, once for the GraphQL schema, and once for the business logic. It feels inefficient, and worse, it’s error-prone. What if you could define your data once and have it work everywhere? That’s why I want to talk about combining TypeGraphQL and TypeORM. If you’re tired of maintaining multiple type definitions, stick with me. This approach might change how you build backends.
Let’s start with the core idea. Instead of separate models, you create one TypeScript class. This single class acts as both your database table definition and your GraphQL type. You use decorators from both libraries to describe what each field means. The result is a unified source of truth for your data shape.
Here’s what that looks like in practice.
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;
}
Look at that. One class, two purposes. The @Entity() and @Column() decorators tell TypeORM this is a database table. The @ObjectType() and @Field() decorators tell TypeGraphQL this is a GraphQL type. The id field is a great example. It’s marked as a primary key in the database and as an ID type in GraphQL. This alignment is the foundation of the whole system.
But a model alone doesn’t do much. You need a way to fetch and manipulate data. This is where resolvers come in. With this setup, your resolver can use TypeORM’s repository directly to interact with the database, and the return type is automatically your GraphQL type.
import { Resolver, Query, Arg } from "type-graphql";
import { User } from "./user.entity";
import { AppDataSource } from "../data-source"; // Your TypeORM setup
@Resolver(User)
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 });
}
}
See how clean that is? The resolver method returns a Promise<User | null>. TypeScript knows exactly what that User type contains. There’s no manual conversion from a database row to a GraphQL object. The types are guaranteed to match because they are the same class. Have you ever spent time debugging why an API returns a null field that definitely exists in the database? This pattern makes those errors a thing of the past.
The real power shows up with relationships. Think about a blog. A User writes many Post entries. In a traditional setup, you’d define this relationship in SQL, then again in your GraphQL schema, and then write resolver logic to join the data. Here, you define it once.
// In the User entity
@OneToMany(() => Post, (post) => post.author)
@Field(() => [Post])
posts: Post[];
// In the Post entity
@ManyToOne(() => User, (user) => user.posts)
@Field(() => User)
author: User;
Now, a GraphQL query can naturally traverse this relationship. You can ask for a user and all their posts in one request. TypeORM handles the database join, and TypeGraphQL handles shaping the response. The resolver logic becomes very simple, often just calling the repository’s find method with the right relations loaded. This eliminates a huge amount of boilerplate code for nested data.
What about creating or updating data? The pattern holds. You define an InputType, which is often a subset of your main entity. Even here, you can reuse your entity’s validation logic.
import { InputType, PickType } from "type-graphql";
@InputType()
export class CreateUserInput extends PickType(User, ["email", "name"]) {}
This CreateUserInput class picks the email and name fields from the User entity. If you add a new required field to the User @Column definition, TypeScript will immediately tell you that CreateUserInput is missing it. This kind of feedback loop is invaluable for maintaining large applications. It turns runtime errors into compile-time warnings.
Is there a catch? Like any abstraction, it requires understanding both tools. You need to know how TypeORM’s lazy and eager loading works to write efficient queries. You must be mindful of the N+1 query problem, which can be solved with a tool like the DataLoader pattern. The integration is powerful, but it doesn’t absolve you from understanding database performance.
For me, the biggest win is confidence. When I change a field type from string to number in my entity, my entire project lights up with TypeScript errors. Every resolver, every input, every query that’s affected is flagged instantly. This makes refactoring safe and fast. It turns the type system from a documentation aid into an active guardian of your codebase.
This approach fits perfectly into a modern development workflow. It works with hot-reload during development. It integrates with testing frameworks, allowing you to mock the database layer easily. It scales from a simple personal project to a large enterprise application because the architecture enforces consistency.
I encourage you to try it. Start with a single entity, like a Product or a Task. Define it with both sets of decorators. Write one query and one mutation. Feel the satisfaction of deleting your separate GraphQL SDL file and DTO classes. You might find, as I did, that it fundamentally simplifies how you think about backend data flow.
If this way of building APIs makes as much sense to you as it does to me, please share this article. Tell me in the comments about your experience. Are you using these tools together? What challenges did you face? Let’s build better, safer backends, one unified model at a time.
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