Build a GraphQL API with TypeGraphQL and TypeORM

Create type-safe, maintainable GraphQL APIs with TypeScript decorators that bridge your database and API layers seamlessly.

Modern web applications require robust, type-safe APIs that can evolve gracefully while maintaining developer productivity. The combination of GraphQL, TypeGraphQL, and TypeORM provides a powerful stack for building Node.js applications with end-to-end type safety. This guide walks through creating a complete GraphQL API using these technologies, demonstrating how they work together to eliminate the traditional friction between your database layer, API schema, and TypeScript types.

GraphQL has transformed how developers think about API design by allowing clients to request exactly the data they need and nothing more. Unlike REST APIs that expose fixed endpoints with predetermined response structures, GraphQL APIs provide a flexible query language that puts the client in control. This approach reduces over-fetching and under-fetching while enabling frontend teams to iterate faster without backend changes.

TypeGraphQL extends this paradigm by allowing you to define your GraphQL schema directly in TypeScript code using decorators. This eliminates the need to maintain separate schema definition language (SDL) files and ensures your schema always matches your resolver implementations. When you change a field in your TypeScript class, TypeGraphQL automatically updates the corresponding GraphQL type, keeping your entire codebase synchronized.

TypeORM complements this approach by providing a type-safe ORM layer for SQL databases. Its decorator-based entity definitions align naturally with TypeGraphQL's approach, creating a seamless development experience from database to API. Together, these tools create a unified codebase where database models, GraphQL types, and TypeScript interfaces are all defined in the same place, reducing cognitive load and eliminating entire categories of bugs.

For teams building modern web applications, this stack provides significant advantages in maintainability and developer experience. The decorator-based approach reduces boilerplate while ensuring type consistency across all layers of your application.

Why This Stack Matters

Key benefits of combining TypeGraphQL with TypeORM

Unified Type System

Single source of truth for database models, GraphQL types, and TypeScript interfaces eliminates code drift and synchronization issues across your [API infrastructure](/services/api-development/).

Decorator-Based Development

TypeScript decorators create a declarative approach to defining entities and resolvers, reducing boilerplate code significantly and improving readability.

Automatic Schema Generation

TypeGraphQL generates GraphQL schema automatically from your TypeScript classes, ensuring schema and code always stay synchronized without manual updates.

Type Safety End-to-End

Full TypeScript type checking from database through API layer catches errors at compile time rather than runtime, reducing production bugs.

Prerequisites

Before diving into the implementation, ensure your development environment is properly configured:

  • JavaScript fundamentals: Solid understanding of JavaScript concepts and patterns
  • Node.js and npm: Experience with package management and running Node.js applications
  • TypeScript basics: Familiarity with types, interfaces, and decorators
  • GraphQL concepts: Understanding of queries, mutations, and schema basics
  • SQL knowledge: Basic understanding of relational databases helps with TypeORM concepts

Required software:

  • Node.js 16 or higher
  • npm or yarn package manager
  • Code editor with TypeScript support (VS Code recommended)

If you're new to TypeScript, consider reviewing our TypeScript tutorial guide before proceeding.

Project Setup

Initializing the Project

Begin by creating a new Node.js project and navigating into its directory:

mkdir graphql-typeorm-api
cd graphql-typeorm-api
npm init -y

Installing Dependencies

The core dependencies bring together three essential libraries:

npm install apollo-server typegraphql typeorm reflect-metadata

Core packages explained:

  • apollo-server: Production-ready GraphQL server implementation that handles query execution and schema management
  • typegraphql: Generates GraphQL schema from TypeScript decorators, eliminating manual SDL definitions
  • typeorm: Type-safe ORM for SQL databases with support for multiple database backends
  • reflect-metadata: Enables TypeScript decorator metadata functionality required by both TypeGraphQL and TypeORM

Development dependencies:

npm install -D typescript ts-node nodemon

TypeScript Configuration

Create tsconfig.json to configure TypeScript's behavior:

{
 "compilerOptions": {
 "target": "es5",
 "module": "commonjs",
 "strict": true,
 "esModuleInterop": true,
 "experimentalDecorators": true,
 "emitDecoratorMetadata": true,
 "strictPropertyInitialization": false
 }
}

Key settings explained:

  • experimentalDecorators and emitDecoratorMetadata: Enable decorator functionality for TypeGraphQL and TypeORM
  • strictPropertyInitialization: false: Allows declaring entity properties without immediate initialization (database-populated values)

Add npm scripts to package.json:

{
 "scripts": {
 "start": "nodemon -w src --ext ts --exec ts-node src/index.ts"
 }
}

This setup provides a solid foundation for building backend services with modern TypeScript practices.

Database Configuration with TypeORM

Understanding TypeORM Configuration

TypeORM supports multiple database types. For development, SQLite provides an excellent starting point because it requires no separate database server installation and creates a simple file-based database. For production applications, PostgreSQL or MySQL offer better scalability, concurrency handling, and operational tooling.

The ormconfig.json file tells TypeORM which database to connect to, where to find your entity definitions, and how to handle schema synchronization. The entities array specifies glob patterns that tell TypeORM where to find your entity class files, while synchronize enables automatic schema creation based on your entity definitions--a useful development feature that should be disabled in production.

SQLite configuration (ormconfig.json):

{
 "type": "sqlite",
 "database": "./database.sqlite",
 "entities": ["src/entities/*.ts"],
 "synchronize": true
}

Creating the Database Connection

The database connection is established in your application's entry point, typically src/index.ts. This file imports reflect-metadata first, which must be loaded before any TypeGraphQL or TypeORM code runs. The createConnection function reads your TypeORM configuration and establishes a connection pool to your database.

import "reflect-metadata";
import { createConnection } from "typeorm";

async function bootstrap() {
 const connection = await createConnection();
 console.log("Database connected successfully");

 // Continue with schema building and server startup
}

Important: reflect-metadata must be imported before any TypeGraphQL or TypeORM code executes.

Choosing Your Database

For production environments, PostgreSQL offers several advantages over SQLite. It handles concurrent connections efficiently, supports advanced query features like window functions and CTEs, and provides robust backup and replication capabilities.

PostgreSQL configuration for production:

npm install pg
{
 "type": "postgres",
 "host": "localhost",
 "port": 5432,
 "username": "your_username",
 "password": "your_password",
 "database": "your_database",
 "entities": ["src/entities/*.ts"],
 "synchronize": true
}

Choosing the right database is an important architectural decision that impacts your application's performance and scalability.

Defining Entities with TypeORM

Entity Fundamentals

Entities in TypeORM are TypeScript classes decorated with @Entity that map to database tables. Each property in the class corresponds to a column in the table, with decorators specifying column types, constraints, and relationships. This approach allows you to work with your database using familiar object-oriented patterns while TypeORM handles the SQL generation behind the scenes.

The @Entity decorator tells TypeORM to create a table with the same name as your class, or you can specify a custom name using the decorator's options. By convention, entity class names are singular and capitalized, while corresponding table names are often pluralized, though TypeORM allows complete control over naming conventions.

Creating Your First Entity

Consider a simple Book entity for demonstrating CRUD operations. This entity includes common fields like id, title, author, and publication year. The @PrimaryGeneratedColumn decorator creates an auto-incrementing primary key, while @Column decorators define the remaining fields with optional type specifications and constraints.

import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";

@Entity()
export class Book {
 @PrimaryGeneratedColumn()
 id: number;

 @Column()
 title: string;

 @Column()
 author: string;

 @Column()
 publishedYear: number;

 @Column({ nullable: true })
 isbn?: string;

 @Column({ default: true })
 isAvailable: boolean;
}

Column Types and Options

TypeORM's @Column decorator supports a wide range of types that map to your database's native column types. String columns can have length limits, numeric columns can specify precision and scale, and date columns can store timestamps with varying levels of granularity.

import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";

@Entity()
export class Article {
 @PrimaryGeneratedColumn()
 id: number;

 @Column({ length: 200 })
 title: string;

 @Column("text")
 content: string;

 @Column("decimal", { precision: 5, scale: 2 })
 price: number;

 @Column({ nullable: true })
 publishedAt?: Date;

 @Column({ default: 0 })
 viewCount: number;
}

Handling Relationships

TypeORM supports all standard SQL relationships including one-to-one, one-to-many, many-to-one, and many-to-many. These relationships are defined using additional decorators like @OneToMany, @ManyToOne, and @JoinColumn, which configure both the TypeScript references and the foreign key constraints in the database.

A common pattern is the one-to-many relationship between an Author entity and a Book entity. The author can have many books, while each book belongs to one author:

import { Entity, PrimaryGeneratedColumn, Column, OneToMany, ManyToOne } from "typeorm";
import { Book } from "./Book";

@Entity()
export class Author {
 @PrimaryGeneratedColumn()
 id: number;

 @Column()
 name: string;

 @Column()
 nationality: string;

 @OneToMany(() => Book, (book) => book.author)
 books: Book[];
}

@Entity()
export class Book {
 @PrimaryGeneratedColumn()
 id: number;

 @Column()
 title: string;

 @ManyToOne(() => Author, (author) => author.books)
 author: Author;
}

Proper entity design is crucial for maintainable database schemas that scale with your application.

Building GraphQL Types with TypeGraphQL

TypeGraphQL Basics

TypeGraphQL enables you to define GraphQL types using TypeScript classes and decorators, eliminating the need for separate SDL files. The framework reads your decorator metadata and automatically generates a complete, valid GraphQL schema. This approach keeps your schema synchronized with your resolver code and leverages TypeScript's type system throughout.

The fundamental building blocks are object types, which correspond to GraphQL object types; queries, which define read operations; and mutations, which define write operations. Each of these is defined using decorators on class properties and methods, with TypeScript types providing additional information about return values and arguments.

Mapping TypeORM Entities to GraphQL Types

The most powerful aspect of combining TypeGraphQL with TypeORM is the ability to map entities directly to GraphQL types. While you can use the same class for both purposes, a cleaner approach often involves separate classes: TypeORM entities for database operations and GraphQL types for API responses. This separation allows you to control exactly what fields are exposed through your API and add computed fields that don't exist in the database.

For simple applications, however, you can use the same class as both an entity and a GraphQL type, leveraging TypeGraphQL's @ObjectType decorator to mark it as a GraphQL type and TypeORM's @Entity decorator to mark it as a database entity:

import { Entity, PrimaryGeneratedColumn, Column, ObjectType, Field, ID, Int } from "typegraphql";

@ObjectType()
@Entity()
export class Book {
 @Field(() => ID)
 @PrimaryGeneratedColumn()
 id: number;

 @Field()
 @Column()
 title: string;

 @Field()
 @Column()
 author: string;

 @Field(() => Int)
 @Column()
 publishedYear: number;

 @Field({ nullable: true })
 @Column({ nullable: true })
 isbn?: string;

 @Field()
 @Column({ default: true })
 isAvailable: boolean;
}

Field Options and Validation

TypeGraphQL's @Field decorator accepts an options object that controls various aspects of how the field appears in your GraphQL schema. The nullable option determines whether the field can return null, the deprecationReason option marks fields as deprecated in your schema, and the description option adds documentation that appears in GraphQL introspection and tooling.

You can also attach validators to fields using decorators from the class-validator library, which integrates seamlessly with TypeGraphQL. These validators run automatically before your resolver functions execute, ensuring incoming data meets your requirements without manual validation logic:

import { ObjectType, Field, ID, Int } from "type-graphql";
import { IsNotEmpty, IsInt, Min, Max, IsOptional, IsBoolean } from "class-validator";

@ObjectType()
export class Book {
 @Field(() => ID)
 id: number;

 @Field()
 @IsNotEmpty({ message: "Title is required" })
 title: string;

 @Field()
 @IsNotEmpty({ message: "Author is required" })
 author: string;

 @Field(() => Int)
 @IsInt({ message: "Publication year must be an integer" })
 @Min(1000, { message: "Publication year must be after 1000" })
 @Max(new Date().getFullYear(), { message: "Publication year cannot be in the future" })
 publishedYear: number;

 @Field({ nullable: true })
 @IsOptional()
 isbn?: string;

 @Field()
 @IsBoolean({ message: "isAvailable must be a boolean" })
 isAvailable: boolean;
}

This validation approach ensures your API endpoints receive clean, validated data before processing.

Creating Resolvers

Resolver Fundamentals

Resolvers in TypeGraphQL are classes decorated with @Resolver that contain methods decorated with @Query or @Mutation. Each method becomes a field in your GraphQL schema, with the decorator specifying whether it's a query (read operation) or mutation (write operation). TypeGraphQL automatically generates the appropriate schema definitions and handles parameter parsing and response formatting.

The resolver class doesn't need to implement any interface--it simply needs to have methods that return the expected types. TypeGraphQL uses TypeScript's type inference to determine the GraphQL return type, and decorators provide any additional metadata needed for the schema.

Building a BookResolver

The BookResolver class handles all GraphQL operations related to books. It includes queries for retrieving single books or lists of books, and mutations for creating, updating, and deleting books. The resolver methods interact with TypeORM's repository to perform database operations, keeping business logic separate from database concerns:

import { Resolver, Query, Mutation, Arg, ID } from "type-graphql";
import { Book } from "../entities/Book";
import { BookRepository } from "../repositories/BookRepository";

@Resolver(() => Book)
export class BookResolver {
 private bookRepository: BookRepository;

 constructor() {
 this.bookRepository = new BookRepository();
 }

 @Query(() => [Book])
 async books(): Promise<Book[]> {
 return this.bookRepository.findAll();
 }

 @Query(() => Book, { nullable: true })
 async book(@Arg("id") id: number): Promise<Book | null> {
 return this.bookRepository.findById(id);
 }

 @Mutation(() => Book)
 async createBook(
 @Arg("title") title: string,
 @Arg("author") author: string,
 @Arg("publishedYear") publishedYear: number,
 @Arg("isbn", { nullable: true }) isbn?: string
 ): Promise<Book> {
 const book = new Book();
 book.title = title;
 book.author = author;
 book.publishedYear = publishedYear;
 book.isbn = isbn;
 book.isAvailable = true;

 return this.bookRepository.save(book);
 }

 @Mutation(() => Book, { nullable: true })
 async updateBook(
 @Arg("id") id: number,
 @Arg("title", { nullable: true }) title?: string,
 @Arg("author", { nullable: true }) author?: string
 ): Promise<Book | null> {
 return this.bookRepository.update(id, { title, author });
 }

 @Mutation(() => Boolean)
 async deleteBook(@Arg("id") id: number): Promise<boolean> {
 return this.bookRepository.delete(id);
 }
}

Using TypeORM Repositories

TypeORM's Repository pattern provides a clean interface for database operations. Each entity has a corresponding repository with methods for common operations like finding records, saving new records, updating existing records, and deleting records. The repository abstracts away the underlying SQL, allowing you to work with familiar object-oriented patterns:

import { getRepository, Repository } from "typeorm";
import { Book } from "../entities/Book";

export class BookRepository {
 private repository: Repository<Book>;

 constructor() {
 this.repository = getRepository(Book);
 }

 async findAll(): Promise<Book[]> {
 return this.repository.find();
 }

 async findById(id: number): Promise<Book | null> {
 return this.repository.findOne({ where: { id } });
 }

 async save(book: Book): Promise<Book> {
 return this.repository.save(book);
 }

 async update(id: number, data: Partial<Book>): Promise<Book | null> {
 await this.repository.update(id, data);
 return this.findById(id);
 }

 async delete(id: number): Promise<boolean> {
 const result = await this.repository.delete(id);
 return (result.affected ?? 0) > 0;
 }
}

Well-designed resolvers are essential for secure and performant APIs.

Server Setup with Apollo Server

Initializing Apollo Server

Apollo Server serves as the execution engine for your GraphQL API, handling incoming requests, parsing queries, executing resolvers, and returning responses. When combined with TypeGraphQL, Apollo Server receives a schema generated from your TypeScript classes, eliminating the need for manual schema construction.

The server initialization occurs in your application's entry point, typically src/index.ts. This file imports all necessary dependencies, builds the schema from your resolvers, creates an Apollo Server instance, and starts the server listening for incoming connections:

import "reflect-metadata";
import { ApolloServer } from "apollo-server";
import { buildSchema } from "type-graphql";
import { createConnection } from "typeorm";
import { BookResolver } from "./resolvers/BookResolver";

async function bootstrap() {
 // Establish database connection
 await createConnection();

 // Build GraphQL schema from resolvers
 const schema = await buildSchema({
 resolvers: [BookResolver],
 validate: true,
 });

 // Create and start Apollo Server
 const server = new ApolloServer({ schema });

 await server.listen(4000);
 console.log("Server has started on http://localhost:4000");
}

bootstrap().catch(console.error);

Enabling Validation

When validate: true is set in buildSchema options, TypeGraphQL runs validation decorators on input arguments before executing resolver methods. This ensures that only valid data reaches your business logic, reducing error handling complexity in your resolvers.

The validation system supports a wide range of decorators including @IsNotEmpty for required fields, @IsEmail for email validation, @Min and @Max for numeric ranges, and @IsIn for enumerated values. Custom validators can be created by implementing the ValidatorConstraintInterface and registering them with class-validator.

Production Configuration

For production deployments, Apollo Server supports several configuration options that enhance reliability and monitoring. The introspection option controls whether the GraphQL introspection query is allowed, which should typically be disabled in production to prevent schema exposure. The playground option enables or disables the GraphQL Playground interface, which is useful during development but should be disabled in production:

const server = new ApolloServer({
 schema,
 introspection: process.env.NODE_ENV !== "production",
 playground: process.env.NODE_ENV !== "production",
 plugins: [
 // Add monitoring and logging plugins here
 ],
});

Proper server configuration is critical for production-ready applications.

Advanced Patterns

Handling Relationships in Resolvers

When working with entity relationships, resolvers must handle both sides of the relationship correctly. For one-to-many relationships, the many side typically includes a foreign key that references the one side. When creating or updating entities, you may need to load the related entity to set the relationship correctly:

@Resolver(() => Book)
export class BookResolver {
 @Query(() => [Book])
 async books(
 @Arg("authorId", { nullable: true }) authorId?: number
 ): Promise<Book[]> {
 const query = Book.createQueryBuilder("book");

 if (authorId) {
 query.where("book.authorId = :authorId", { authorId });
 }

 return query.getMany();
 }

 @Mutation(() => Book)
 async createBookWithAuthor(
 @Arg("title") title: string,
 @Arg("authorName") authorName: string,
 @Arg("authorNationality") authorNationality: string
 ): Promise<Book> {
 const author = await Author.create({
 name: authorName,
 nationality: authorNationality,
 }).save();

 const book = await Book.create({
 title,
 author,
 }).save();

 return book;
 }
}

Pagination

Real-world APIs often require pagination to handle large datasets efficiently. Cursor-based pagination provides better performance for large datasets by using an indexed column (typically the primary key) to identify the starting point. Offset-based pagination is simpler to implement but performs worse on large datasets:

@Query(() => [Book])
async books(
 @Arg("limit", () => Int, { defaultValue: 10 }) limit: number,
 @Arg("cursor", { nullable: true }) cursor?: number
): Promise<Book[]> {
 const query = Book.createQueryBuilder("book")
 .orderBy("book.id", "ASC")
 .take(limit + 1);

 if (cursor) {
 query.where("book.id > :cursor", { cursor });
 }

 const books = await query.getMany();

 const hasNextPage = books.length > limit;
 if (hasNextPage) {
 books.pop();
 }

 return books;
}

Error Handling

Robust error handling ensures your API provides useful feedback when things go wrong. TypeGraphQL works with Apollo Server's error formatting system to provide structured error responses. Custom error classes can extend ApolloError to provide specific error codes and messages:

import { ApolloError } from "apollo-server";

export class NotFoundError extends ApolloError {
 constructor(message: string) {
 super(message, "NOT_FOUND");
 }
}

export class ValidationError extends ApolloError {
 constructor(message: string) {
 super(message, "VALIDATION_ERROR");
 }
}

// Usage in resolver
@Query(() => Book, { nullable: true })
async book(@Arg("id") id: number): Promise<Book | null> {
 const book = await Book.findOne({ where: { id } });

 if (!book) {
 throw new NotFoundError(`Book with id ${id} not found`);
 }

 return book;
}

These patterns form the foundation for scalable API architectures.

Conclusion

Building a GraphQL API with TypeGraphQL and TypeORM provides a powerful, type-safe foundation for modern Node.js applications. The combination eliminates the traditional separation between database models, API schemas, and TypeScript types, creating a unified codebase that's easier to maintain and less prone to errors. Decorators bridge the gap between your TypeScript classes and the generated GraphQL schema and database tables, ensuring consistency across all layers of your application.

The patterns demonstrated here--entity definitions, resolver construction, server setup, and advanced patterns like pagination and error handling--provide a foundation for building production-ready APIs. As your application grows, you can extend these patterns to include:

  • Authentication and authorization for secure API access using JWT or OAuth
  • Caching strategies with Redis to improve performance for frequently accessed data
  • Real-time subscriptions for live data updates and notifications
  • Custom field resolvers for computed properties and aggregated data
  • Interfaces and unions for polymorphic types and conditional rendering

The type-safe nature of this stack makes it particularly valuable in large codebases where maintaining consistency would otherwise require significant effort. By keeping your database models, GraphQL types, and resolvers synchronized through shared TypeScript classes and decorators, you reduce cognitive load and eliminate entire categories of bugs that typically arise from schema-code drift.

Next steps for your project:

  1. Explore TypeGraphQL subscriptions for real-time features
  2. Implement authentication with JSON Web Tokens for secure access
  3. Add caching with Redis for frequently accessed data
  4. Set up logging and monitoring for production observability
  5. Consider NestJS integration for larger applications with dependency injection

If you're building enterprise applications, this stack integrates well with our enterprise web development services for scalable, maintainable solutions.

Frequently Asked Questions

Can I use TypeGraphQL with NoSQL databases like MongoDB?

While TypeGraphQL is designed primarily for SQL databases through TypeORM, you can use it with MongoDB using libraries like typegoose. However, the type safety benefits are most pronounced with SQL databases where TypeORM's entity mappings provide the strongest guarantees. For NoSQL-focused projects, consider alternative approaches that better match your database model.

How does TypeGraphQL handle database migrations?

TypeORM's synchronize feature automatically creates database schema from entities during development, which is convenient but not recommended for production. For production environments, use TypeORM's migration system to generate and run migration scripts that evolve your schema safely without data loss. This approach gives you full control over schema changes and supports team collaboration.

What's the difference between using repositories and the Active Record pattern?

Repositories provide a Data Mapper approach where entities are plain objects and repository methods handle database operations. The Active Record pattern embeds database methods directly on entities. Both work with TypeGraphQL; repositories typically scale better for complex applications with many entities and relationships, while Active Record offers simpler syntax for smaller projects.

How do I handle authentication in TypeGraphQL resolvers?

Use TypeGraphQL's middlewares or custom decorators to intercept resolver execution. Check for authentication tokens in the context and throw errors for unauthenticated requests. Field-level authorization can be implemented similarly using custom field resolvers. This approach keeps your authentication logic DRY and maintainable across many resolvers.

Can I use this stack with NestJS?

Yes! NestJS has official integration with both TypeGraphQL and TypeORM. The framework provides additional structure for large applications including modules, dependency injection, and built-in testing utilities. Many enterprise projects benefit from NestJS's opinionated architecture when building scalable GraphQL APIs with TypeGraphQL and TypeORM.

Sources

  1. LogRocket: Building GraphQL APIs with TypeGraphQL and TypeORM - Comprehensive tutorial covering the entire stack setup with code examples for CRUD API implementation
  2. TypeGraphQL Official Documentation - Getting Started - Official framework documentation for decorators and schema building
  3. EasyDevv: TypeORM with GraphQL (type-graphql) and Express - Integration patterns between TypeORM and GraphQL
  4. Medium: Mastering GraphQL APIs with TypeGraphQL, TypeORM and Express in Node.js - Comprehensive guide to building production-ready APIs

Ready to Build Your Type-Safe GraphQL API?

Our team specializes in building modern, type-safe web applications using the latest technologies and best practices. Let's discuss how we can help you implement a robust API architecture.