├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── README.md ├── nest-cli.json ├── package-lock.json ├── package.json ├── src ├── __mocks__ │ ├── mockUserSettings.ts │ └── mockUsers.ts ├── app.module.ts ├── graphql │ ├── models │ │ ├── User.ts │ │ └── UserSetting.ts │ ├── resolvers │ │ └── UserSettingsResolver.ts │ └── utils │ │ ├── CreateUserInput.ts │ │ └── CreateUserSettingsInput.ts ├── main.ts ├── schema.gql ├── users │ ├── UserResolver.ts │ ├── UserService.ts │ ├── UserSettingService.ts │ └── users.module.ts └── utils │ └── queries.ts ├── test ├── app.e2e-spec.ts └── jest-e2e.json ├── tsconfig.build.json └── tsconfig.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir: __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | pnpm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # OS 15 | .DS_Store 16 | 17 | # Tests 18 | /coverage 19 | /.nyc_output 20 | 21 | # IDEs and editors 22 | /.idea 23 | .project 24 | .classpath 25 | .c9/ 26 | *.launch 27 | .settings/ 28 | *.sublime-workspace 29 | 30 | # IDE - VSCode 31 | .vscode/* 32 | !.vscode/settings.json 33 | !.vscode/tasks.json 34 | !.vscode/launch.json 35 | !.vscode/extensions.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NestJS + GraphQL + TypeORM + SQL 2 | 3 | Use this repository as a template or resource for NestJS with GraphQL and TypeORM. 4 | Check out the video tutorial [here](https://www.youtube.com/watch?v=CSfZmyzQAG8&). 5 | [![NestJS GraphQL with TypeORM & SQL](https://github.com/stuyy/nestjs-graphql-typeorm/assets/25330491/935f8740-2f1b-4cc6-9275-5c62cf63ceb7)](https://www.youtube.com/watch?v=CSfZmyzQAG8&) 6 | 7 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-tutorial-nestjs", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "build": "nest build", 10 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 11 | "start": "nest start", 12 | "start:dev": "nest start --watch", 13 | "start:debug": "nest start --debug --watch", 14 | "start:prod": "node dist/main", 15 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 16 | "test": "jest", 17 | "test:watch": "jest --watch", 18 | "test:cov": "jest --coverage", 19 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 20 | "test:e2e": "set NODE_ENV=TEST&& jest --config ./test/jest-e2e.json" 21 | }, 22 | "dependencies": { 23 | "@apollo/server": "^4.9.5", 24 | "@nestjs/apollo": "^12.0.11", 25 | "@nestjs/common": "^10.0.0", 26 | "@nestjs/core": "^10.0.0", 27 | "@nestjs/graphql": "^12.0.11", 28 | "@nestjs/platform-express": "^10.0.0", 29 | "@nestjs/typeorm": "^10.0.1", 30 | "graphql": "^16.8.1", 31 | "graphql-tag": "^2.12.6", 32 | "mysql2": "^3.6.5", 33 | "reflect-metadata": "^0.1.13", 34 | "rxjs": "^7.8.1", 35 | "typeorm": "^0.3.17" 36 | }, 37 | "devDependencies": { 38 | "@nestjs/cli": "^10.0.0", 39 | "@nestjs/schematics": "^10.0.0", 40 | "@nestjs/testing": "^10.0.0", 41 | "@types/express": "^4.17.17", 42 | "@types/jest": "^29.5.2", 43 | "@types/node": "^20.3.1", 44 | "@types/supertest": "^2.0.12", 45 | "@typescript-eslint/eslint-plugin": "^6.0.0", 46 | "@typescript-eslint/parser": "^6.0.0", 47 | "eslint": "^8.42.0", 48 | "eslint-config-prettier": "^9.0.0", 49 | "eslint-plugin-prettier": "^5.0.0", 50 | "jest": "^29.5.0", 51 | "prettier": "^3.0.0", 52 | "source-map-support": "^0.5.21", 53 | "supertest": "^6.3.3", 54 | "ts-jest": "^29.1.0", 55 | "ts-loader": "^9.4.3", 56 | "ts-node": "^10.9.1", 57 | "tsconfig-paths": "^4.2.0", 58 | "typescript": "^5.1.3" 59 | }, 60 | "jest": { 61 | "moduleFileExtensions": [ 62 | "js", 63 | "json", 64 | "ts" 65 | ], 66 | "rootDir": "src", 67 | "testRegex": ".*\\.spec\\.ts$", 68 | "transform": { 69 | "^.+\\.(t|j)s$": "ts-jest" 70 | }, 71 | "collectCoverageFrom": [ 72 | "**/*.(t|j)s" 73 | ], 74 | "coverageDirectory": "../coverage", 75 | "testEnvironment": "node" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/__mocks__/mockUserSettings.ts: -------------------------------------------------------------------------------- 1 | export const mockUserSettings = [ 2 | { 3 | userId: 1, 4 | receiveNotifications: false, 5 | receiveEmails: false, 6 | }, 7 | { 8 | userId: 3, 9 | receiveNotifications: true, 10 | receiveEmails: false, 11 | }, 12 | ]; 13 | -------------------------------------------------------------------------------- /src/__mocks__/mockUsers.ts: -------------------------------------------------------------------------------- 1 | export const mockUsers = [ 2 | { 3 | id: 1, 4 | username: 'anson', 5 | displayName: 'Anson The Developer', 6 | }, 7 | { 8 | id: 2, 9 | username: 'jack', 10 | displayName: 'Jack The Developer', 11 | }, 12 | { 13 | id: 3, 14 | username: 'peter', 15 | displayName: 'Peter The Developer', 16 | }, 17 | ]; 18 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { ApolloDriverConfig, ApolloDriver } from '@nestjs/apollo'; 2 | import { Module } from '@nestjs/common'; 3 | import { GraphQLModule } from '@nestjs/graphql'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { UserSettingsResolver } from './graphql/resolvers/UserSettingsResolver'; 6 | import { User } from './graphql/models/User'; 7 | import { UserSetting } from './graphql/models/UserSetting'; 8 | import { UsersModule } from './users/users.module'; 9 | 10 | @Module({ 11 | imports: [ 12 | GraphQLModule.forRoot({ 13 | driver: ApolloDriver, 14 | autoSchemaFile: 'src/schema.gql', 15 | }), 16 | TypeOrmModule.forRoot({ 17 | type: 'mysql', 18 | host: 'localhost', 19 | port: 3306, 20 | username: 'testuser', 21 | password: 'testuser123', 22 | database: 23 | process.env.NODE_ENV === 'TEST' 24 | ? 'graphql_tutorial_test' 25 | : 'graphql_tutorial', 26 | entities: [User, UserSetting], 27 | synchronize: true, 28 | logging: false, 29 | }), 30 | UsersModule, 31 | ], 32 | controllers: [], 33 | providers: [], 34 | }) 35 | export class AppModule {} 36 | -------------------------------------------------------------------------------- /src/graphql/models/User.ts: -------------------------------------------------------------------------------- 1 | import { ObjectType, Field, Int } from '@nestjs/graphql'; 2 | import { 3 | Entity, 4 | Column, 5 | PrimaryGeneratedColumn, 6 | OneToOne, 7 | JoinColumn, 8 | } from 'typeorm'; 9 | import { UserSetting } from './UserSetting'; 10 | 11 | @Entity({ name: 'users' }) 12 | @ObjectType() 13 | export class User { 14 | @PrimaryGeneratedColumn() 15 | @Field((type) => Int) 16 | id: number; 17 | 18 | @Column() 19 | @Field() 20 | username: string; 21 | 22 | @Column({ nullable: true }) 23 | @Field({ nullable: true }) 24 | displayName?: string; 25 | 26 | @OneToOne(() => UserSetting) 27 | @JoinColumn() 28 | @Field({ nullable: true }) 29 | settings?: UserSetting; 30 | } 31 | -------------------------------------------------------------------------------- /src/graphql/models/UserSetting.ts: -------------------------------------------------------------------------------- 1 | import { PrimaryColumn, Column, Entity } from 'typeorm'; 2 | import { ObjectType, Field, Int } from '@nestjs/graphql'; 3 | 4 | @Entity({ name: 'user_settings' }) 5 | @ObjectType() 6 | export class UserSetting { 7 | @PrimaryColumn() 8 | @Field((type) => Int) 9 | userId: number; 10 | 11 | @Column({ default: false }) 12 | @Field({ defaultValue: false }) 13 | receiveNotifications: boolean; 14 | 15 | @Column({ default: false }) 16 | @Field({ defaultValue: false }) 17 | receiveEmails: boolean; 18 | } 19 | -------------------------------------------------------------------------------- /src/graphql/resolvers/UserSettingsResolver.ts: -------------------------------------------------------------------------------- 1 | import { Resolver, Mutation, Args } from '@nestjs/graphql'; 2 | import { UserSetting } from '../models/UserSetting'; 3 | import { CreateUserSettingsInput } from '../utils/CreateUserSettingsInput'; 4 | import { mockUserSettings } from '../../__mocks__/mockUserSettings'; 5 | import { UserSettingService } from '../../users/UserSettingService'; 6 | 7 | @Resolver() 8 | export class UserSettingsResolver { 9 | constructor(private userSettingsService: UserSettingService) {} 10 | 11 | @Mutation((returns) => UserSetting) 12 | async createUserSettings( 13 | @Args('createUserSettingsData') 14 | createUserSettingsData: CreateUserSettingsInput, 15 | ) { 16 | const userSetting = await this.userSettingsService.createUserSettings( 17 | createUserSettingsData, 18 | ); 19 | return userSetting; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/graphql/utils/CreateUserInput.ts: -------------------------------------------------------------------------------- 1 | import { InputType, Field } from '@nestjs/graphql'; 2 | 3 | @InputType() 4 | export class CreateUserInput { 5 | @Field() 6 | username: string; 7 | 8 | @Field({ nullable: true }) 9 | displayName?: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/graphql/utils/CreateUserSettingsInput.ts: -------------------------------------------------------------------------------- 1 | import { InputType, Field, Int } from '@nestjs/graphql'; 2 | 3 | @InputType() 4 | export class CreateUserSettingsInput { 5 | @Field((type) => Int) 6 | userId: number; 7 | 8 | @Field({ nullable: true, defaultValue: false }) 9 | receiveNotifications: boolean; 10 | 11 | @Field({ nullable: true, defaultValue: false }) 12 | receiveEmails: boolean; 13 | } 14 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | 4 | async function bootstrap() { 5 | const app = await NestFactory.create(AppModule); 6 | await app.listen(3000); 7 | } 8 | bootstrap(); 9 | -------------------------------------------------------------------------------- /src/schema.gql: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------ 2 | # THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY) 3 | # ------------------------------------------------------ 4 | 5 | type UserSetting { 6 | userId: Int! 7 | receiveNotifications: Boolean! 8 | receiveEmails: Boolean! 9 | } 10 | 11 | type User { 12 | id: Int! 13 | username: String! 14 | displayName: String 15 | settings: UserSetting 16 | } 17 | 18 | type Query { 19 | getUserById(id: Int!): User 20 | getUsers: [User!]! 21 | } 22 | 23 | type Mutation { 24 | createUser(createUserData: CreateUserInput!): User! 25 | createUserSettings(createUserSettingsData: CreateUserSettingsInput!): UserSetting! 26 | } 27 | 28 | input CreateUserInput { 29 | username: String! 30 | displayName: String 31 | } 32 | 33 | input CreateUserSettingsInput { 34 | userId: Int! 35 | receiveNotifications: Boolean = false 36 | receiveEmails: Boolean = false 37 | } -------------------------------------------------------------------------------- /src/users/UserResolver.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Resolver, 3 | Query, 4 | Args, 5 | Int, 6 | ResolveField, 7 | Parent, 8 | Mutation, 9 | } from '@nestjs/graphql'; 10 | import { User } from '../graphql/models/User'; 11 | import { mockUsers } from '../__mocks__/mockUsers'; 12 | import { UserSetting } from '../graphql/models/UserSetting'; 13 | import { mockUserSettings } from '../__mocks__/mockUserSettings'; 14 | import { CreateUserInput } from '../graphql/utils/CreateUserInput'; 15 | import { Inject } from '@nestjs/common'; 16 | import { UserService } from './UserService'; 17 | import { UserSettingService } from './UserSettingService'; 18 | 19 | export let incrementalId = 3; 20 | 21 | @Resolver((of) => User) 22 | export class UserResolver { 23 | constructor( 24 | private userService: UserService, 25 | private userSettingService: UserSettingService, 26 | ) {} 27 | 28 | @Query((returns) => User, { nullable: true }) 29 | getUserById(@Args('id', { type: () => Int }) id: number) { 30 | return this.userService.getUserById(id); 31 | } 32 | 33 | @Query(() => [User]) 34 | getUsers() { 35 | return this.userService.getUsers(); 36 | } 37 | 38 | // @ResolveField((returns) => UserSetting, { name: 'settings', nullable: true }) 39 | // getUserSettings(@Parent() user: User) { 40 | // return this.userSettingService.getUserSettingById(user.id); 41 | // } 42 | 43 | @Mutation((returns) => User) 44 | createUser(@Args('createUserData') createUserData: CreateUserInput) { 45 | return this.userService.createUser(createUserData); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/users/UserService.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { Repository } from 'typeorm'; 4 | import { User } from '../graphql/models/User'; 5 | import { CreateUserInput } from '../graphql/utils/CreateUserInput'; 6 | 7 | @Injectable() 8 | export class UserService { 9 | constructor( 10 | @InjectRepository(User) private usersRepository: Repository, 11 | ) {} 12 | 13 | getUsers() { 14 | return this.usersRepository.find({ relations: ['settings'] }); 15 | } 16 | 17 | getUserById(id: number) { 18 | return this.usersRepository.findOne({ 19 | where: { id }, 20 | relations: ['settings'], 21 | }); 22 | } 23 | 24 | createUser(createUserData: CreateUserInput) { 25 | const newUser = this.usersRepository.create(createUserData); 26 | return this.usersRepository.save(newUser); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/users/UserSettingService.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { User } from '../graphql/models/User'; 4 | import { UserSetting } from '../graphql/models/UserSetting'; 5 | import { CreateUserSettingsInput } from '../graphql/utils/CreateUserSettingsInput'; 6 | import { Repository } from 'typeorm'; 7 | 8 | @Injectable() 9 | export class UserSettingService { 10 | constructor( 11 | @InjectRepository(UserSetting) 12 | private userSettingsRepository: Repository, 13 | @InjectRepository(User) 14 | private userRepository: Repository, 15 | ) {} 16 | 17 | getUserSettingById(userId: number) { 18 | return this.userSettingsRepository.findOneBy({ userId }); 19 | } 20 | 21 | async createUserSettings(createUserSettingsData: CreateUserSettingsInput) { 22 | const findUser = await this.userRepository.findOneBy({ 23 | id: createUserSettingsData.userId, 24 | }); 25 | 26 | if (!findUser) throw new Error('User Not Found'); 27 | 28 | const newUserSetting = this.userSettingsRepository.create( 29 | createUserSettingsData, 30 | ); 31 | const savedSettings = 32 | await this.userSettingsRepository.save(newUserSetting); 33 | 34 | findUser.settings = savedSettings; 35 | await this.userRepository.save(findUser); 36 | 37 | return savedSettings; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/users/users.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { UserResolver } from './UserResolver'; 4 | import { UserService } from './UserService'; 5 | import { User } from '../graphql/models/User'; 6 | import { UserSettingService } from './UserSettingService'; 7 | import { UserSetting } from '../graphql/models/UserSetting'; 8 | import { UserSettingsResolver } from '../graphql/resolvers/UserSettingsResolver'; 9 | 10 | @Module({ 11 | imports: [TypeOrmModule.forFeature([User, UserSetting])], 12 | providers: [ 13 | UserResolver, 14 | UserService, 15 | UserSettingService, 16 | UserSettingsResolver, 17 | ], 18 | }) 19 | export class UsersModule {} 20 | -------------------------------------------------------------------------------- /src/utils/queries.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export const createUserMutation = gql` 4 | mutation { 5 | createUser(createUserData: { username: "anson", displayName: "Anson" }) { 6 | id 7 | username 8 | displayName 9 | } 10 | } 11 | `; 12 | 13 | export const getUsersQuery = gql` 14 | { 15 | getUsers { 16 | id 17 | username 18 | displayName 19 | } 20 | } 21 | `; 22 | -------------------------------------------------------------------------------- /test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | import { DataSource } from 'typeorm'; 6 | import { print } from 'graphql'; 7 | import { createUserMutation, getUsersQuery } from '../src/utils/queries'; 8 | 9 | describe('GraphQL Server (e2e)', () => { 10 | let app: INestApplication; 11 | 12 | beforeAll(async () => { 13 | const moduleFixture: TestingModule = await Test.createTestingModule({ 14 | imports: [AppModule], 15 | }).compile(); 16 | 17 | app = moduleFixture.createNestApplication(); 18 | const dataSource = app.get(DataSource); 19 | await dataSource.synchronize(true); 20 | await app.init(); 21 | }); 22 | 23 | afterAll(async () => { 24 | const dataSource = app.get(DataSource); 25 | if (dataSource) { 26 | await dataSource.dropDatabase(); 27 | await dataSource.destroy(); 28 | } 29 | await app.close(); 30 | }); 31 | 32 | describe('users', () => { 33 | it('should query getUsers and return 0 users', () => { 34 | return request(app.getHttpServer()) 35 | .post('/graphql') 36 | .send({ query: print(getUsersQuery) }) 37 | .expect((res) => { 38 | expect(res.body.data.getUsers).toHaveLength(0); 39 | }); 40 | }); 41 | 42 | it('should create a user using createUser mutation', () => { 43 | return request(app.getHttpServer()) 44 | .post('/graphql') 45 | .send({ 46 | query: print(createUserMutation), 47 | }) 48 | .expect(200) 49 | .expect((res) => { 50 | expect(res.body.data.createUser).toEqual({ 51 | id: 1, 52 | username: 'anson', 53 | displayName: 'Anson', 54 | }); 55 | }); 56 | }); 57 | 58 | it('should query getUsers and return 1 users', () => { 59 | return request(app.getHttpServer()) 60 | .post('/graphql') 61 | .send({ query: print(getUsersQuery) }) 62 | .expect((res) => { 63 | expect(res.body.data.getUsers).toHaveLength(1); 64 | }); 65 | }); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "ES2021", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": false, 16 | "noImplicitAny": false, 17 | "strictBindCallApply": false, 18 | "forceConsistentCasingInFileNames": false, 19 | "noFallthroughCasesInSwitch": false 20 | } 21 | } 22 | --------------------------------------------------------------------------------