Introduction
\n\nGraphQL is a query language for APIs that allows clients to request only the data they need. Combined with TypeScript, it becomes a powerful tool for creating type-safe, scalable, and documented APIs. In this step-by-step tutorial, we will build a full GraphQL API in TypeScript using Apollo Server and TypeGraphQL.
You will learn:
\n- \n
- How to set up a project with TypeScript and Apollo Server \n
- How to define a GraphQL schema using TypeGraphQL decorators \n
- How to write resolvers with full typing \n
- How to connect a database and execute queries \n
This material will be useful for both beginners and developers already familiar with REST APIs who want to learn a modern approach to building APIs.
\n\n1. Project Setup and Dependency Installation
\n\nLet's start by initializing the project. Make sure you have Node.js (version 16+) and npm/yarn installed.
\n\nmkdir graphql-ts-tutorial\ncd graphql-ts-tutorial\nnpm init -y\n\nInstall the required packages:
\n\nnpm install apollo-server graphql reflect-metadata type-graphql class-validator\nnpm install -D typescript @types/node ts-node\n\nCreate the tsconfig.json file:
{\n "compilerOptions": {\n "target": "ES2020",\n "module": "commonjs",\n "lib": ["ES2020"],\n "experimentalDecorators": true,\n "emitDecoratorMetadata": true,\n "strict": true,\n "esModuleInterop": true,\n "outDir": "./dist",\n "rootDir": "./src"\n },\n "include": ["src/**/*"]\n}\n\nNote the experimentalDecorators and emitDecoratorMetadata flags — they are required for TypeGraphQL to work.
2. Defining the Schema and Types with TypeGraphQL
\n\nLet's create the src folder and the file src/types/User.ts. Here we will describe the user model.
import { ObjectType, Field, ID, InputType } from "type-graphql";\n\n@ObjectType()\nexport class User {\n @Field(() => ID)\n id!: number;\n\n @Field()\n name!: string;\n\n @Field()\n email!: string;\n\n @Field()\n createdAt!: Date;\n}\n\nExplanation:
\n- \n
@ObjectType()— marks the class as a GraphQL type. \n @Field()— specifies which fields will be available in the API. \n @Field(() => ID)— for the scalar ID type. \n
Now let's create an InputType for mutations (creating a user):
@InputType()\nexport class CreateUserInput {\n @Field()\n name!: string;\n\n @Field()\n email!: string;\n}\n\nThis type will be used in resolvers for input data validation.
\n\n3. Writing Resolvers
\n\nLet's create the file src/resolvers/UserResolver.ts. Here we will implement the query and mutation logic.
import { Resolver, Query, Mutation, Arg } from "type-graphql";\nimport { User, CreateUserInput } from "../types/User";\n\n// Simulated database\nlet users: User[] = [\n { id: 1, name: "Ivan Ivanov", email: "ivan@example.com", createdAt: new Date() },\n { id: 2, name: "Maria Petrova", email: "maria@example.com", createdAt: new Date() }\n];\n\nlet nextId = 3;\n\n@Resolver()\nexport class UserResolver {\n @Query(() => [User])\n async users(): Promise<User[]> {\n return users;\n }\n\n @Query(() => User, { nullable: true })\n async user(@Arg("id") id: number): Promise<User | undefined> {\n return users.find(u => u.id === id);\n }\n\n @Mutation(() => User)\n async createUser(@Arg("data") data: CreateUserInput): Promise<User> {\n const newUser: User = {\n id: nextId++,\n name: data.name,\n email: data.email,\n createdAt: new Date()\n };\n users.push(ne