js

How to Build Type-Safe GraphQL APIs with TypeORM and TypeGraphQL

Unify your backend by using TypeScript classes as both GraphQL types and database models. Learn how to simplify and scale your API.

How to Build Type-Safe GraphQL APIs with TypeORM and TypeGraphQL

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

Keywords: graphql,typescript,typegraphql,typeorm,backend development



Similar Posts
Blog Image
Complete Guide to Integrating Next.js with Prisma ORM for Type-Safe Database Operations

Learn to integrate Next.js with Prisma ORM for type-safe, scalable web apps. Build seamless database interactions with modern tools. Start coding today!

Blog Image
Build Type-Safe Event-Driven Architecture: TypeScript, EventEmitter3, and Redis Pub/Sub Guide

Master TypeScript Event-Driven Architecture with Redis Pub/Sub. Learn type-safe event systems, distributed scaling, CQRS patterns & production best practices.

Blog Image
Complete Guide to Integrating Next.js with Prisma ORM for Type-Safe Full-Stack Development

Learn how to integrate Next.js with Prisma ORM for type-safe, scalable full-stack apps. Build modern web applications with seamless database operations.

Blog Image
Complete Multi-Tenant SaaS Guide: NestJS, Prisma, Row-Level Security Implementation

Learn to build scalable multi-tenant SaaS apps with NestJS, Prisma & PostgreSQL RLS. Complete guide with authentication, tenant isolation & performance tips.

Blog Image
Build a High-Performance GraphQL API with Fastify Mercurius and Redis Caching Tutorial

Build a high-performance GraphQL API with Fastify, Mercurius & Redis caching. Learn advanced optimization, data loaders, and production deployment strategies.

Blog Image
How to Simplify API Calls in Nuxt 3 Using Ky for Cleaner Code

Streamline your Nuxt 3 data fetching with Ky—centralized config, universal support, and cleaner error handling. Learn how to set it up now.