Introduction to GraphQL API with TypeScript
GraphQL is a modern query language for APIs that allows clients to request only the data they actually need. Combined with TypeScript, which adds strict typing, development becomes more predictable and safer. In this step-by-step tutorial, we will create a full-fledged GraphQL API in TypeScript using popular tools and best practices.
Why choose GraphQL with TypeScript? First, you get autocompletion and type checking at compile time. Second, GraphQL allows combining multiple queries into one, reducing the number of network requests. Third, TypeScript is ideal for describing complex graph data structures.
Project setup and dependency installation
To start, let's create a new project and install the necessary packages. We will use Apollo Server — the most popular server for GraphQL, and TypeGraphQL for convenient integration with TypeScript.
mkdir graphql-ts-tutorialcd graphql-ts-tutorialnpm init -ynpm install apollo-server graphql reflect-metadata class-validatornpm install typescript type-graphql @types/node --save-devnpx tsc --initAfter installation, configure tsconfig.json. Make sure the following options are enabled:
{ "compilerOptions": { "target": "ES2020", "module": "commonjs", "lib": ["ES2020"], "experimentalDecorators": true, "emitDecoratorMetadata": true, "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true }}Create the folder structure:
src/ ├── index.ts ├── resolvers/ │ └── user.resolver.ts ├── schemas/ │ └── user.schema.ts └── data/ └── users.tsCreating the schema and data types
In TypeGraphQL, data types are described using decorators. Let's create a user model and an input type for mutations.
// src/schemas/user.schema.tsimport { ObjectType, Field, ID, InputType } from "type-graphql";
@ObjectType()export class User { @Field(() => ID) id!: number;
@Field() name!: string;
@Field() email!: string;
@Field({ nullable: true }) age?: number;}
@InputType()export class CreateUserInput { @Field() name!: string;
@Field() email!: string;
@Field({ nullable: true }) age?: number;}Note the use of @ObjectType() to describe return types and @InputType() for input parameters. This allows TypeGraphQL to automatically generate the GraphQL schema.
Implementing resolvers
Resolvers are functions that process queries and return data. Let's create a simple in-memory array to store users.
// src/data/users.tsimport { User } from "../schemas/user.schema";
export const users: User[] = [ { id: 1, name: "Ivan Petrov", email: "ivan@example.com", age: 28 }, { id: 2, name: "Maria Sidorova", email: "maria@example.com", age: 32 },];Now let's write resolvers for queries and mutations:
// src/resolvers/user.resolver.tsimport { Resolver, Query, Mutation, Arg, Int } from "type-graphql";import { User, CreateUserInput } from "../schemas/user.schema";import { users } from "../data/users";
@Resolver()export class UserResolver { @Query(() => [User]) async getAllUsers(): Promise<User[]> { return users; }
@Query(() => User, { nullable: true }) async getUserById( @Arg("id", () => Int) id: number ): Promise<User | undefined> { return users.find(user => user.id === id); }
@Mutation(() => User) async createUser( @Arg("input") input: CreateUserInput ): Promise<User> { const newUser: User =