I’ve been thinking about this topic a lot lately. Every time I build a GraphQL API, I find myself writing the same types twice—once for the database and once for the GraphQL schema. It feels like wasted effort, and it’s easy for things to get out of sync. What if you could define your data model once and have everything just work? That’s exactly what brought me to explore TypeGraphQL with TypeORM. This combination changes how you build GraphQL servers in Node.js. If you’re tired of boilerplate and want true type safety from your database to your API client, stick with me. I’ll show you how to build something better.
Let’s start with the basics. TypeGraphQL uses TypeScript decorators to define your GraphQL schema directly in your code. TypeORM does the same for your database. When you combine them, you write your model once. This single source of truth means your types are always consistent. You get automatic GraphQL schema generation and a type-safe way to work with your database. The result is less code and fewer bugs.
Why does this matter? In a typical project, you might define a User type in GraphQL SDL and then create a similar User entity for TypeORM. When you add a new field, you must update both places. Forget one, and your app breaks. This stack removes that duplication. Your entity class is your GraphQL type. Changes happen in one file.
How do we set this up? First, create a new project. You’ll need a few key packages.
npm init -y
npm install express apollo-server-express type-graphql typeorm reflect-metadata pg
npm install -D typescript @types/node ts-node-dev
The reflect-metadata package is crucial. It allows TypeScript decorators to work properly. Don’t forget to import it at the very top of your main entry file.
Next, configure TypeScript. Your tsconfig.json needs specific settings.
{
"compilerOptions": {
"target": "ES2020",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"strictPropertyInitialization": false
}
}
The experimentalDecorators and emitDecoratorMetadata flags are non-negotiable. They enable the decorator magic. strictPropertyInitialization: false is a practical choice for TypeORM entities, which often initialize properties at runtime.
Now, let’s define our first entity. Imagine we’re building a blog. We need a User and a Post. Here’s how you define a User that works for both GraphQL and the database.
import { ObjectType, Field, ID } from 'type-graphql';
import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm';
import { Post } from './Post';
@ObjectType()
@Entity()
export class User {
@Field(() => ID)
@PrimaryGeneratedColumn('uuid')
id: string;
@Field()
@Column({ unique: true })
email: string;
@Field()
@Column()
username: string;
@Column()
password: string;
@Field(() => [Post])
@OneToMany(() => Post, post => post.author)
posts: Post[];
}
See how the decorators pair up? @ObjectType() and @Entity() mark the class. @Field() and @Column() define properties. Notice the password field has only @Column(). It won’t be exposed in the GraphQL schema. This is a simple, secure way to hide sensitive data.
What about the Post entity? It will reference the User.
import { ObjectType, Field, ID } from 'type-graphql';
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm';
import { User } from './User';
@ObjectType()
@Entity()
export class Post {
@Field(() => ID)
@PrimaryGeneratedColumn('uuid')
id: string;
@Field()
@Column()
title: string;
@Field()
@Column('text')
content: string;
@Field(() => User)
@ManyToOne(() => User, user => user.posts)
author: User;
}
The relationship is defined with @ManyToOne. The @Field(() => User) tells TypeGraphQL that this will return a User object. But here’s a question: when you fetch a list of posts, do you want the database to fetch the author for every single post individually? Probably not. That leads to the “N+1 query problem,” which can crush your performance.
This is where DataLoader becomes essential. It batches and caches database calls. Let’s create a simple loader for users.
import DataLoader from 'dataloader';
import { User } from '../entities/User';
import { AppDataSource } from '../database';
const batchUsers = async (userIds: string[]) => {
const users = await AppDataSource.getRepository(User).findByIds(userIds);
const userMap: { [key: string]: User } = {};
users.forEach(user => {
userMap[user.id] = user;
});
return userIds.map(id => userMap[id]);
};
export const userLoader = () => new DataLoader(batchUsers);
The loader takes a list of user IDs and fetches them all in one database query. Then, it maps them back to the correct order. You would use this inside your resolver.
Speaking of resolvers, how do we create them? With TypeGraphQL, you use a resolver class. Here’s a basic resolver for our Post entity.
import { Resolver, Query, Arg, Mutation } from 'type-graphql';
import { Post } from '../entities/Post';
import { CreatePostInput } from '../inputs/CreatePostInput';
import { AppDataSource } from '../database';
@Resolver(Post)
export class PostResolver {
private postRepository = AppDataSource.getRepository(Post);
@Query(() => [Post])
async posts(): Promise<Post[]> {
return this.postRepository.find({ relations: ['author'] });
}
@Query(() => Post, { nullable: true })
async post(@Arg('id') id: string): Promise<Post | null> {
return this.postRepository.findOne({ where: { id }, relations: ['author'] });
}
@Mutation(() => Post)
async createPost(@Arg('data') data: CreatePostInput): Promise<Post> {
const post = this.postRepository.create(data);
return this.postRepository.save(post);
}
}
The @Resolver(Post) decorator tells TypeGraphQL this class handles the Post type. The @Query and @Mutation decorators define the GraphQL operations. Notice the CreatePostInput. This is a special class for mutation arguments.
Why use a separate input class? It keeps your schema clean. You might not want to expose every entity field in a create operation. Here’s what it looks like.
import { InputType, Field } from 'type-graphql';
import { IsNotEmpty, Length } from 'class-validator';
@InputType()
export class CreatePostInput {
@Field()
@IsNotEmpty()
@Length(3, 100)
title: string;
@Field()
@IsNotEmpty()
content: string;
@Field()
authorId: string;
}
We’re using class-validator decorators like @IsNotEmpty(). TypeGraphQL can automatically validate input against these rules before the resolver even runs. This built-in validation is a huge time-saver.
Now, we need to connect everything in our server file. This is where we build the schema and start Apollo Server.
import 'reflect-metadata';
import express from 'express';
import { ApolloServer } from 'apollo-server-express';
import { buildSchema } from 'type-graphql';
import { PostResolver } from './resolvers/PostResolver';
import { UserResolver } from './resolvers/UserResolver';
import { initializeDatabase } from './database';
async function startServer() {
// 1. Connect to the database
await initializeDatabase();
// 2. Build the GraphQL schema automatically from resolvers
const schema = await buildSchema({
resolvers: [PostResolver, UserResolver],
validate: true,
});
// 3. Create Apollo Server
const server = new ApolloServer({
schema,
context: () => ({
// We can pass things like loaders or user info here
userLoader: userLoader(),
}),
});
await server.start();
const app = express();
server.applyMiddleware({ app });
app.listen({ port: 4000 }, () =>
console.log(`Server ready at http://localhost:4000${server.graphqlPath}`)
);
}
startServer().catch(console.error);
The buildSchema function is the heart of TypeGraphQL. It scans your resolver classes and generates the complete GraphQL schema. The context function lets you attach utilities like our DataLoader to every request. How might you use that loader in a resolver? You’d access it from the context argument.
Let’s look at a more advanced resolver method that uses the loader.
@Resolver()
export class PostResolver {
// ... other methods
@Query(() => [Post])
async postsWithAuthors(@Ctx() ctx: any): Promise<Post[]> {
const posts = await this.postRepository.find();
// Imagine we need to load authors separately
const authorIds = posts.map(p => p.authorId);
const authors = await ctx.userLoader.loadMany(authorIds);
// ... merge data
return posts;
}
}
The @Ctx() decorator gives you access to the context. You can see how the loader is used. But wait, isn’t there a better way? Yes. With our setup, we defined the author as a relation on the Post entity. If we use the find method with the relations option, TypeORM will handle the join for us. DataLoader is more useful for complex, nested queries where multiple separate batches are needed.
What about security? You can easily add authorization by creating custom middleware. TypeGraphQL supports middleware at the resolver, field, or global level.
import { MiddlewareFn } from 'type-graphql';
import { MyContext } from '../types/MyContext'; // Your custom context type
export const isAuth: MiddlewareFn<MyContext> = ({ context }, next) => {
if (!context.currentUser) {
throw new Error('Not authenticated');
}
return next();
};
You can then use this on a resolver or query.
@Query(() => [Post])
@UseMiddleware(isAuth)
async myPosts(@Ctx() ctx: MyContext): Promise<Post[]> {
return this.postRepository.find({ where: { authorId: ctx.currentUser.id } });
}
This pattern keeps your authorization logic clean and reusable. You can imagine building more complex rules, like checking user roles.
Finally, let’s talk about production. A few tips: First, never use synchronize: true in your TypeORM configuration for a production database. It can drop data. Use migrations instead. Second, consider query complexity limiting. A malicious user could ask for deeply nested data. Apollo Server has plugins to help with this. Third, always enable GraphQL playground only in development.
The journey from duplicate type definitions to a single, type-safe source of truth is transformative. You write less code. Your types are always in sync. Your development speed increases. The initial setup has a few steps, but the long-term payoff is massive. You stop worrying about schema mismatches and focus on building features.
I hope this guide helps you see the potential of this stack. It solved a real, frustrating problem in my own work. If you’ve struggled with keeping your GraphQL schema and database models aligned, give TypeGraphQL and TypeORM a try. Start with a simple entity, see how the decorators work together, and build from there.
Did you find this approach to building GraphQL APIs useful? Have you tried similar stacks? I’d love to hear about your experiences or answer any questions. If this guide helped you, please consider sharing it with other developers who might be facing the same challenges. Let’s build more robust and type-safe applications 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