├── .eslintignore ├── .gitignore ├── .prettierignore ├── docs └── images │ ├── depending_diagram.png │ └── relation_between_domains.png ├── .prettierrc.js ├── src ├── entities │ └── user.entity.ts ├── IoC │ ├── icradle.interface.ts │ ├── container.ts │ └── providers │ │ ├── auth.provider.ts │ │ └── user.provider.ts ├── persistence │ ├── constants.ts │ ├── password.ts │ ├── auth.repository.ts │ └── user.repository.ts ├── application │ ├── graphql │ │ ├── schemaShards │ │ │ ├── index.ts │ │ │ └── users.ts │ │ ├── utils │ │ │ └── mergeRawSchemas.ts │ │ ├── subscriptionManager.ts │ │ └── index.ts │ ├── index.ts │ └── auth │ │ └── index.ts ├── services │ ├── auth.repository.interface.ts │ ├── user.repository.interface.ts │ ├── user.service.ts │ └── auth.service.ts ├── dto │ └── user.dto.ts ├── index.ts └── __typedefs │ ├── schema.graphql │ └── graphqlTypes.d.ts ├── .graphqlrc.yml ├── tsconfig.json ├── .github └── FUNDING.yml ├── .eslintrc.js ├── LICENSE ├── package.json └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | # build artefacts 2 | dist/* 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node build artifacts 2 | node_modules 3 | npm-debug.log 4 | dist 5 | .idea -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | lib 3 | .gitignore 4 | *.snap 5 | *.svg 6 | *.png 7 | *.jpeg 8 | *.jpg 9 | *.md -------------------------------------------------------------------------------- /docs/images/depending_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuanlc/graphql_clean-architecture_boilerplate/HEAD/docs/images/depending_diagram.png -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: "es5", 4 | singleQuote: true, 5 | printWidth: 120, 6 | tabWidth: 2 7 | }; 8 | -------------------------------------------------------------------------------- /src/entities/user.entity.ts: -------------------------------------------------------------------------------- 1 | export default interface UserEntity { 2 | id: string; 3 | name: string; 4 | email: string; 5 | password: string; 6 | } 7 | -------------------------------------------------------------------------------- /docs/images/relation_between_domains.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuanlc/graphql_clean-architecture_boilerplate/HEAD/docs/images/relation_between_domains.png -------------------------------------------------------------------------------- /.graphqlrc.yml: -------------------------------------------------------------------------------- 1 | schema: 'http://localhost:3000/graphql' 2 | extensions: 3 | codegen: 4 | generates: 5 | ./src/__typedefs/schema.graphql: 6 | plugins: 7 | - schema-ast 8 | -------------------------------------------------------------------------------- /src/IoC/icradle.interface.ts: -------------------------------------------------------------------------------- 1 | import { IAuthProvider } from './providers/auth.provider'; 2 | import { IUserProvider } from './providers/user.provider'; 3 | 4 | export default interface ICradle extends IAuthProvider, IUserProvider {} 5 | -------------------------------------------------------------------------------- /src/persistence/constants.ts: -------------------------------------------------------------------------------- 1 | export const DB = { 2 | MODEL_NAMES: { 3 | USER: 'users', 4 | }, 5 | }; 6 | 7 | export const AUTHENTICATED_USER_TOKEN_TTL = 86400; // (in second) 1 day = 24 x 60 x 60 8 | 9 | export const JWT_SECRET_KEY = 'secret'; 10 | -------------------------------------------------------------------------------- /src/application/graphql/schemaShards/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file merges all of the schemas that belong to different parts of the shards 3 | */ 4 | import users from './users'; 5 | import { mergeRawSchemas } from '../utils/mergeRawSchemas'; 6 | 7 | export default mergeRawSchemas(users); 8 | -------------------------------------------------------------------------------- /src/services/auth.repository.interface.ts: -------------------------------------------------------------------------------- 1 | import { AuthenticatedUserDTO, PublicUserDTO } from '../dto/user.dto'; 2 | 3 | export default interface AuthRepositoryInterface { 4 | getAuthenticatedUserByToken(token: string): Promise; 5 | 6 | generateToken(user: PublicUserDTO): Promise; 7 | } 8 | -------------------------------------------------------------------------------- /src/application/index.ts: -------------------------------------------------------------------------------- 1 | import { ApolloServer } from '@apollo/server'; 2 | import { makeExecutableSchema } from 'graphql-tools'; 3 | import { rawSchema } from './graphql'; 4 | 5 | // create our schema 6 | const schema = makeExecutableSchema(rawSchema); 7 | 8 | // create a new server 9 | export default new ApolloServer({ 10 | schema, 11 | }); 12 | -------------------------------------------------------------------------------- /src/dto/user.dto.ts: -------------------------------------------------------------------------------- 1 | import UserEntity from '../entities/user.entity'; 2 | 3 | export interface CreateUserDTO { 4 | email: string; 5 | name: string; 6 | password: string; 7 | } 8 | 9 | export type PublicUserDTO = Omit; 10 | 11 | export interface AuthenticatedUserDTO extends PublicUserDTO { 12 | token: string; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { startStandaloneServer } from '@apollo/server/standalone'; 2 | import server from './application'; 3 | 4 | startStandaloneServer(server, { 5 | context: async ({ req }) => ({ token: req.headers.token }), 6 | listen: { port: parseInt(process.env.PORT || '0') || 3000 }, 7 | }).then(({ url }) => { 8 | console.log(`🚀 Server ready at ${url}`); 9 | }); 10 | -------------------------------------------------------------------------------- /src/services/user.repository.interface.ts: -------------------------------------------------------------------------------- 1 | import { CreateUserDTO, PublicUserDTO } from '../dto/user.dto'; 2 | 3 | export default interface UserRepositoryInterface { 4 | getUserById(id: string): Promise; 5 | 6 | authenticateUser(email: string, password: string): Promise; 7 | 8 | createUser(data: CreateUserDTO): Promise; 9 | } 10 | -------------------------------------------------------------------------------- /src/persistence/password.ts: -------------------------------------------------------------------------------- 1 | import bcrypt from 'bcrypt'; 2 | 3 | const saltRounds = 10; 4 | 5 | export const hashPassword = (plainPassword: string): Promise => { 6 | return bcrypt.hash(plainPassword, saltRounds); 7 | }; 8 | 9 | export const checkPassword = (plainPassword: string, hashedPassword: string): Promise => { 10 | return bcrypt.compare(plainPassword, hashedPassword); 11 | }; 12 | -------------------------------------------------------------------------------- /src/IoC/container.ts: -------------------------------------------------------------------------------- 1 | import { createContainer, InjectionMode } from 'awilix'; 2 | 3 | import ICradle from './icradle.interface'; 4 | import authProvider from './providers/auth.provider'; 5 | import userProvider from './providers/user.provider'; 6 | 7 | const container = createContainer({ 8 | injectionMode: InjectionMode.CLASSIC, 9 | }); 10 | 11 | authProvider(container); 12 | userProvider(container); 13 | 14 | export default container; 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "lib": ["ES2020"], 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "target": "ES2020", 8 | "outDir": "./dist", 9 | "strict": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "esModuleInterop": true 13 | }, 14 | "include": ["src"], 15 | "exclude": ["*.test.ts"], 16 | "ts-node": { 17 | "files": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/application/auth/index.ts: -------------------------------------------------------------------------------- 1 | import container from '../../IoC/container'; 2 | 3 | export interface IContext { 4 | token?: string; 5 | } 6 | 7 | export async function authenticateContext(context: IContext): Promise { 8 | if (!context.token) { 9 | throw new Error('user is not logged in'); 10 | } 11 | const user = await container.cradle.authService.authenticateUserByToken(context.token); 12 | if (!user) { 13 | throw new Error('invalid token'); 14 | } 15 | return user; 16 | } 17 | -------------------------------------------------------------------------------- /src/application/graphql/utils/mergeRawSchemas.ts: -------------------------------------------------------------------------------- 1 | import { IExecutableSchemaDefinition } from '@graphql-tools/schema'; 2 | import mergeWith from 'lodash.mergewith'; 3 | 4 | function withArraysConcatination(objValue: unknown, srcValue: unknown) { 5 | // if an array, concat it 6 | if (Array.isArray(objValue)) { 7 | return objValue.concat(srcValue); 8 | } 9 | } 10 | 11 | // allows us to merge schemas 12 | export const mergeRawSchemas = (...schemas: IExecutableSchemaDefinition[]): IExecutableSchemaDefinition => { 13 | return mergeWith({}, ...schemas, withArraysConcatination); 14 | }; 15 | -------------------------------------------------------------------------------- /src/application/graphql/subscriptionManager.ts: -------------------------------------------------------------------------------- 1 | import { PubSub } from 'graphql-subscriptions'; 2 | 3 | // In a production server you might want to have some message broker or pubsub implementation like 4 | // rabbitMQ, redis or kafka logic here 5 | // you can use one of the graphql subscription implementations to do it easily 6 | // 7 | // Redis: https://github.com/davidyaha/graphql-redis-subscriptions 8 | // Kafka: https://github.com/ancashoria/graphql-kafka-subscriptions 9 | // Rabbitmq: https://github.com/cdmbase/graphql-rabbitmq-subscriptions 10 | 11 | export const pubsub = new PubSub(); 12 | -------------------------------------------------------------------------------- /src/application/graphql/index.ts: -------------------------------------------------------------------------------- 1 | import { mergeRawSchemas } from './utils/mergeRawSchemas'; 2 | import schemaShards from './schemaShards'; 3 | import gql from 'graphql-tag'; 4 | 5 | export const rawSchema = mergeRawSchemas( 6 | { 7 | typeDefs: [ 8 | // we create empty main types, we can later extend them in the shards 9 | gql` 10 | type Query { 11 | _empty: String 12 | } 13 | 14 | type Mutation { 15 | _empty: String 16 | } 17 | 18 | type Subscription { 19 | _empty: String 20 | } 21 | `, 22 | ], 23 | resolvers: {}, 24 | }, 25 | schemaShards 26 | ); 27 | -------------------------------------------------------------------------------- /src/IoC/providers/auth.provider.ts: -------------------------------------------------------------------------------- 1 | import { asClass, AwilixContainer } from 'awilix'; 2 | import AuthRepository from '../../persistence/auth.repository'; 3 | import AuthService from '../../services/auth.service'; 4 | 5 | import ICradle from '../icradle.interface'; 6 | 7 | export interface IAuthProvider { 8 | authRepository: AuthRepository; 9 | authService: AuthService; 10 | } 11 | 12 | const authProvider = (container: AwilixContainer): void => { 13 | // Register the classes 14 | container.register({ 15 | authRepository: asClass(AuthRepository), 16 | authService: asClass(AuthService), 17 | }); 18 | }; 19 | 20 | export default authProvider; 21 | -------------------------------------------------------------------------------- /src/IoC/providers/user.provider.ts: -------------------------------------------------------------------------------- 1 | import { asClass, AwilixContainer } from 'awilix'; 2 | import UserRepository from '../../persistence/user.repository'; 3 | import UserService from '../../services/user.service'; 4 | 5 | import ICradle from '../icradle.interface'; 6 | 7 | export interface IUserProvider { 8 | userRepository: UserRepository; 9 | userService: UserService; 10 | } 11 | 12 | const userProvider = (container: AwilixContainer): void => { 13 | // Register the classes 14 | container.register({ 15 | userRepository: asClass(UserRepository), 16 | userService: asClass(UserService), 17 | }); 18 | }; 19 | 20 | export default userProvider; 21 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: tlcong 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /src/services/user.service.ts: -------------------------------------------------------------------------------- 1 | import { AuthenticatedUserDTO, CreateUserDTO, PublicUserDTO } from '../dto/user.dto'; 2 | import AuthRepositoryInterface from './auth.repository.interface'; 3 | import UserRepositoryInterface from './user.repository.interface'; 4 | 5 | export default class UserService { 6 | constructor(private userRepository: UserRepositoryInterface, private authRepository: AuthRepositoryInterface) {} 7 | 8 | getUserById(userId: string): Promise { 9 | return this.userRepository.getUserById(userId); 10 | } 11 | 12 | async register(data: CreateUserDTO): Promise { 13 | const createdUser = await this.userRepository.createUser(data); 14 | 15 | const token = await this.authRepository.generateToken(createdUser); 16 | 17 | return { 18 | ...createdUser, 19 | token, 20 | }; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/services/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { AuthenticatedUserDTO } from '../dto/user.dto'; 2 | import AuthRepositoryInterface from './auth.repository.interface'; 3 | import UserRepositoryInterface from './user.repository.interface'; 4 | 5 | export default class AuthService { 6 | constructor(private authRepository: AuthRepositoryInterface, private userRepository: UserRepositoryInterface) {} 7 | 8 | async authenticateUser(email: string, password: string): Promise { 9 | const authenticatedUser = await this.userRepository.authenticateUser(email, password); 10 | 11 | if (!authenticatedUser) return; 12 | 13 | const token = await this.authRepository.generateToken(authenticatedUser); 14 | 15 | return { 16 | ...authenticatedUser, 17 | token, 18 | }; 19 | } 20 | 21 | async authenticateUserByToken(token: string): Promise { 22 | return this.authRepository.getAuthenticatedUserByToken(token); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/__typedefs/schema.graphql: -------------------------------------------------------------------------------- 1 | """ 2 | used for logging in 3 | """ 4 | input InputLogin { 5 | email: String! 6 | password: String! 7 | } 8 | 9 | """ 10 | used for creating a new user 11 | """ 12 | input InputRegisterUser { 13 | name: String! 14 | email: String! 15 | password: String! 16 | } 17 | 18 | type Mutation { 19 | _empty: String 20 | 21 | """ 22 | register a new user 23 | """ 24 | registerUser(input: InputRegisterUser!): User 25 | } 26 | 27 | """ 28 | a type defining a user's public data 29 | """ 30 | type PublicUser { 31 | id: ID 32 | name: String 33 | email: String 34 | } 35 | 36 | type Query { 37 | _empty: String 38 | 39 | """ 40 | login as a user 41 | """ 42 | loginUser(input: InputLogin!): User 43 | 44 | """ 45 | get a user's public data 46 | """ 47 | getUser(id: ID!): PublicUser 48 | } 49 | 50 | type Subscription { 51 | _empty: String 52 | } 53 | 54 | """ 55 | a type defining a user 56 | """ 57 | type User { 58 | id: ID 59 | name: String 60 | email: String 61 | token: String 62 | } 63 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', // Specifies the ESLint parser 3 | parserOptions: { 4 | ecmaVersion: 2019, // Allows for the parsing of modern ECMAScript features 5 | sourceType: 'module', // Allows for the use of imports 6 | }, 7 | extends: [ 8 | 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin 9 | 'prettier/@typescript-eslint', // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier 10 | 'plugin:prettier/recommended', // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array. 11 | ], 12 | rules: { 13 | // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs 14 | // e.g. "@typescript-eslint/explicit-function-return-type": "off", 15 | '@typescript-eslint/no-explicit-any': 1, 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023-present, Tuan LE CONG 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/persistence/auth.repository.ts: -------------------------------------------------------------------------------- 1 | import { AuthenticatedUserDTO, PublicUserDTO } from '../dto/user.dto'; 2 | import AuthRepositoryInterface from '../services/auth.repository.interface'; 3 | import { 4 | JsonWebTokenError, 5 | NotBeforeError, 6 | sign as signJWT, 7 | TokenExpiredError, 8 | verify as verifyJWT, 9 | } from 'jsonwebtoken'; 10 | import { AUTHENTICATED_USER_TOKEN_TTL, JWT_SECRET_KEY } from './constants'; 11 | 12 | export default class AuthRepository implements AuthRepositoryInterface { 13 | async getAuthenticatedUserByToken(token: string): Promise { 14 | try { 15 | const decodedData = verifyJWT(token, JWT_SECRET_KEY); 16 | const { id, email, name } = decodedData as PublicUserDTO; 17 | 18 | if (!id || !email) { 19 | return; 20 | } 21 | 22 | return { 23 | id, 24 | email, 25 | name, 26 | token, 27 | }; 28 | } catch (err) { 29 | if (err instanceof TokenExpiredError || err instanceof JsonWebTokenError || err instanceof NotBeforeError) { 30 | return; 31 | } 32 | 33 | throw err; 34 | } 35 | } 36 | 37 | async generateToken(user: PublicUserDTO): Promise { 38 | return signJWT(user, JWT_SECRET_KEY, { expiresIn: AUTHENTICATED_USER_TOKEN_TTL }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/persistence/user.repository.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidV4 } from 'uuid'; 2 | import { CreateUserDTO, PublicUserDTO } from '../dto/user.dto'; 3 | import UserRepositoryInterface from '../services/user.repository.interface'; 4 | import { checkPassword, hashPassword } from './password'; 5 | import UserEntity from '../entities/user.entity'; 6 | 7 | const users: UserEntity[] = []; 8 | 9 | export default class UserRepository implements UserRepositoryInterface { 10 | async getUserById(id: string): Promise { 11 | const user = users.find((item) => item.id === id); 12 | 13 | if (!user) return; 14 | 15 | return this.normalizeUser(user); 16 | } 17 | 18 | async authenticateUser(email: string, password: string): Promise { 19 | const user = users.find((item) => item.email === email); 20 | 21 | if (!user) return; 22 | 23 | const isCorrectPassword = await checkPassword(password, user.password); 24 | 25 | if (!isCorrectPassword) return; 26 | 27 | return this.normalizeUser(user); 28 | } 29 | 30 | async createUser(data: CreateUserDTO): Promise { 31 | const userToCreate = { 32 | id: uuidV4(), 33 | email: data.email, 34 | name: data.name, 35 | password: await hashPassword(data.password), 36 | }; 37 | 38 | await users.push(userToCreate); 39 | 40 | return this.normalizeUser(userToCreate); 41 | } 42 | 43 | normalizeUser(rawUser: UserEntity): PublicUserDTO { 44 | return { 45 | id: rawUser.id, 46 | email: rawUser.email, 47 | name: rawUser.name, 48 | }; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/application/graphql/schemaShards/users.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | import container from '../../../IoC/container'; 3 | 4 | const typeDefs = gql` 5 | extend type Query { 6 | " login as a user " 7 | loginUser(input: InputLogin!): User 8 | " get a user's public data" 9 | getUser(id: ID!): PublicUser 10 | } 11 | 12 | extend type Mutation { 13 | " register a new user " 14 | registerUser(input: InputRegisterUser!): User 15 | } 16 | 17 | " used for logging in " 18 | input InputLogin { 19 | email: String! 20 | password: String! 21 | } 22 | 23 | " used for creating a new user " 24 | input InputRegisterUser { 25 | name: String! 26 | email: String! 27 | password: String! 28 | } 29 | 30 | " a type defining a user's public data " 31 | type PublicUser { 32 | id: ID 33 | name: String 34 | email: String 35 | } 36 | 37 | " a type defining a user " 38 | type User { 39 | id: ID 40 | name: String 41 | email: String 42 | token: String 43 | } 44 | `; 45 | 46 | export default { 47 | resolvers: { 48 | Query: { 49 | // login 50 | loginUser: (root: unknown, { input: { email, password } }: GQL.QueryToLoginUserArgs): unknown => 51 | container.cradle.authService.authenticateUser(email, password), 52 | // get a user 53 | getUser: (root: unknown, { id }: GQL.QueryToGetUserArgs): unknown => container.cradle.userService.getUserById(id), 54 | }, 55 | Mutation: { 56 | // register 57 | registerUser: (root: unknown, { input }: GQL.MutationToRegisterUserArgs): unknown => 58 | container.cradle.userService.register(input), 59 | }, 60 | }, 61 | typeDefs: [typeDefs], 62 | }; 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql_clean-architecture_boilerplate", 3 | "version": "0.0.1", 4 | "description": "GraphQL + Clean Architecture boilerplate", 5 | "main": "index.js", 6 | "author": "tuancnttbk93@gmail.com", 7 | "license": "MIT", 8 | "scripts": { 9 | "dev": "ts-node-dev --respawn --transpile-only src/index.ts", 10 | "lint": "eslint . --ext .ts", 11 | "lint:auto-fix": "eslint . --ext .ts --quiet --fix", 12 | "prettier:fix": "npx prettier --write 'src/**/*'", 13 | "build:ts": "tsc", 14 | "build:clean": "rimraf ./dist", 15 | "build": "npm run build:clean && npm run build:ts", 16 | "start": "node dist/index.js", 17 | "create-schema": "graphql codegen", 18 | "create-types": "graphql-schema-typescript --namespace=GQL --global=true --typePrefix='' generate-ts --output=src/__typedefs/graphqlTypes.d.ts src/__typedefs", 19 | "generate-typedefs": "npm run create-schema && npm run create-types", 20 | "debug": "graphql-schema-typescript --help" 21 | }, 22 | "dependencies": { 23 | "@apollo/server": "^4.5.0", 24 | "@graphql-tools/schema": "^9.0.17", 25 | "awilix": "^4.3.4", 26 | "bcrypt": "^5.1.0", 27 | "cors": "^2.8.5", 28 | "graphql-subscriptions": "^2.0.0", 29 | "graphql-tag": "^2.12.6", 30 | "graphql-tools": "^8.3.19", 31 | "jsonwebtoken": "^9.0.0", 32 | "lodash.mergewith": "^4.6.2", 33 | "uuid": "^9.0.0" 34 | }, 35 | "//": "Temporarily use the forked version to fix graphql16.x. @jlowcs/graphql-schema-typescript", 36 | "//": "https://github.com/dangcuuson/graphql-schema-typescript/pull/67", 37 | "devDependencies": { 38 | "@graphql-codegen/schema-ast": "^3.0.1", 39 | "@types/bcrypt": "^5.0.0", 40 | "@types/cors": "^2.8.13", 41 | "@types/jsonwebtoken": "^9.0.1", 42 | "@types/lodash.mergewith": "^4.6.6", 43 | "@types/node": "^10.12.20", 44 | "@types/uuid": "^9.0.1", 45 | "@typescript-eslint/eslint-plugin": "^4.5.0", 46 | "@typescript-eslint/parser": "^4.5.0", 47 | "eslint": "^7.11.0", 48 | "eslint-config-prettier": "^6.14.0", 49 | "eslint-plugin-prettier": "^3.1.4", 50 | "graphql": "^16.8.1", 51 | "graphql-cli": "^4.1.0", 52 | "@jlowcs/graphql-schema-typescript": "^2.0.0", 53 | "nodemon": "^1.18.9", 54 | "prettier": "^2.8.4", 55 | "ts-loader": "^4.5.0", 56 | "ts-node-dev": "^1.1.6", 57 | "typescript": "^4.9.5" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GraphQL + Clean Architectire boilerplate 2 | 3 | I wrote an article about this boilerplate at: 4 | - Medium: https://congtuanle.medium.com/graphql-clean-architectire-boilerplate-1beb07935b41 5 | - Or dev.to: https://dev.to/tuanlc/graphql-clean-architectire-boilerplate-hog 6 | 7 | Discussion & Comments are warmly welcomed! 8 | 9 | Feel free to send PRs to improve this repository. I hope this work will save time for you and the people in the community who are considering to adopt Graphql + Typescript + Clean Architeture. 10 | 11 | ## Inspiration 12 | The [Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) is an awesome guide for developers who desire to build a clean, structured and maintainable projects. The letter L in SOLID principal allows us to make depended components are easily replaced without touch to the business and the core of systems. For example of the web server where there are many frameworks out there (Expressjs, GraphQL, etc) and the chosen decision depends on the purpose of the business and the context. 13 | 14 | In this boilerplate guide scope we will go throght how to make the core business immune with the change of the detailed technologies. Let's go! 15 | 16 | ## Boilerplate 17 | ### Code structure 18 | ``` 19 | . 20 | ├── package.json 21 | ├── package-lock.json 22 | ├── README.md 23 | ├── src 24 | │ ├── application 25 | │ │ ├── auth 26 | │ │ │ └── index.ts 27 | │ │ ├── graphql 28 | │ │ │ ├── index.ts 29 | │ │ │ ├── schemaShards 30 | │ │ │ │ ├── index.ts 31 | │ │ │ │ └── users.ts 32 | │ │ │ ├── subscriptionManager.ts 33 | │ │ │ └── utils 34 | │ │ │ └── mergeRawSchemas.ts 35 | │ │ └── index.ts 36 | │ ├── dto 37 | │ │ └── user.dto.ts 38 | │ ├── entities 39 | │ │ └── user.entity.ts 40 | │ ├── index.ts 41 | │ ├── IoC 42 | │ │ ├── container.ts 43 | │ │ ├── icradle.interface.ts 44 | │ │ └── providers 45 | │ │ ├── auth.provider.ts 46 | │ │ └── user.provider.ts 47 | │ ├── persistence 48 | │ │ ├── auth.repository.ts 49 | │ │ ├── constants.ts 50 | │ │ ├── password.ts 51 | │ │ └── user.repository.ts 52 | │ ├── services 53 | │ │ ├── auth.repository.interface.ts 54 | │ │ ├── auth.service.ts 55 | │ │ ├── user.repository.interface.ts 56 | │ │ └── user.service.ts 57 | │ └── __typedefs 58 | │ ├── graphqlTypes.d.ts 59 | │ └── schema.graphql 60 | └── tsconfig.json 61 | ``` 62 | 63 | The main folder is `src` where contains the implementation: 64 | - **application**: Application layer where we can use a backend web application framework. In this boilerplate is `GraphQL` 65 | - **service**: The business service layer where we defined the logic to handle application usecases. This layers also contains adapters that abstract the detailed actions, for example, access to database. 66 | - **entities**: This is the corest component of the application where we define the application entities. 67 | - **persistence**: This is another detailed layer where we specify actions to communicate to database, message queue, etc. 68 | - **dto**: (stand for Data Transfer Object) defines communication contracts between layers. For example, what type of input params or returned value that is used in methods of layers 69 | - **IoC**: (stand for Iversion of Control) is an implementation of the Dependency Injection, the letter D in the SOLID principal. Everytime you create a new Service class, or a new repository class, or a new controller, you need to register them in this container. This boilerplate uses [Awilix](https://github.com/jeffijoe/awilix) library to achieve the factor. 70 | 71 | ### Communication Policies 72 | The communication policies between layers and domains compliant with the Clean Architecture. The directions of arrows in the following diagrams show the relation between components. For example, if an arrow direction from component X to component Y, it means component X depends on the component Y. 73 | 74 | 1. Dependency diagram 75 | ![Dependency diagram](./docs/images/depending_diagram.png) 76 | 77 | As you can see in the diagram the dependency flow among layers: 78 | - Entity is the core layer and does not depend on any component 79 | - To inverse the dependency from the domain business layer to the repository layer, there is a repository interface in the domain business. This allows us to be able to implement and update this layer with the detailed technologies 80 | - Application Controller depends in the services 81 | - IoC container depends on services, repositories and application controllers because we need to register all of them to the container 82 | - Application router depends on the IoC container that allows us to use registered application controllers to handle client requests 83 | 84 | 2. Dependency between domains 85 | ![Dependency between domains diagram](./docs/images/relation_between_domains.png) 86 | 87 | In real-life projects, of course, there is not merely 1 domain. So, we need to define the communication rules between layers of them. 88 | 89 | ## Getting Started 90 | There are some beginning available commandlines is defined the `scripts` part of the `package.json` file. 91 | 92 | 1. Install dependencies 93 | ```bash 94 | npm ci 95 | ``` 96 | 97 | 2. Run the application under the `dev` mode with the hot reload feature 98 | ```bash 99 | npm run dev 100 | ``` 101 | 102 | 3. Compile the application 103 | ```bash 104 | npm run build 105 | ``` 106 | 107 | 4. Run the applicatino under the `production` mode. You need to compile the application before 108 | ```bash 109 | npm run start 110 | ``` 111 | 112 | 5. Generate types for GraphQL schema. This action **must be done** each time you update the GraphQL schema to allow typescript compiler understand your schema: 113 | ```bash 114 | npm run generate-typedefs 115 | ``` 116 | 117 | 6. Lint 118 | ```bash 119 | npm run lint 120 | npm run lint:auto-fix 121 | ``` 122 | 123 | 7. Prettier 124 | ```bash 125 | npm run prettier:fix 126 | ``` 127 | 128 | ## References 129 | - [Node + Typescript + MongoDB GraphQL API Starter Kit](https://anthonyriera.medium.com/you-just-found-the-best-node-typescript-mongodb-graphql-api-starter-kit-1f6f53d841cb) 130 | - [The Clean Architecture Blog](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) 131 | -------------------------------------------------------------------------------- /src/__typedefs/graphqlTypes.d.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | import { GraphQLResolveInfo } from 'graphql'; 4 | /** 5 | * This file is auto-generated by @jlowcs/graphql-schema-typescript 6 | * Please note that any changes in this file may be overwritten 7 | */ 8 | 9 | export {}; 10 | 11 | declare global { 12 | namespace GQL { 13 | /******************************* 14 | * * 15 | * TYPE DEFS * 16 | * * 17 | *******************************/ 18 | /** 19 | * used for logging in 20 | */ 21 | export interface InputLogin { 22 | email: string; 23 | password: string; 24 | } 25 | 26 | /** 27 | * used for creating a new user 28 | */ 29 | export interface InputRegisterUser { 30 | name: string; 31 | email: string; 32 | password: string; 33 | } 34 | 35 | export interface Mutation { 36 | _empty?: string; 37 | 38 | /** 39 | * register a new user 40 | */ 41 | registerUser?: User; 42 | } 43 | 44 | /** 45 | * a type defining a user's public data 46 | */ 47 | export interface PublicUser { 48 | id?: string; 49 | name?: string; 50 | email?: string; 51 | } 52 | 53 | export interface Query { 54 | _empty?: string; 55 | 56 | /** 57 | * login as a user 58 | */ 59 | loginUser?: User; 60 | 61 | /** 62 | * get a user's public data 63 | */ 64 | getUser?: PublicUser; 65 | } 66 | 67 | export interface Subscription { 68 | _empty?: string; 69 | } 70 | 71 | /** 72 | * a type defining a user 73 | */ 74 | export interface User { 75 | id?: string; 76 | name?: string; 77 | email?: string; 78 | token?: string; 79 | } 80 | 81 | /********************************* 82 | * * 83 | * TYPE RESOLVERS * 84 | * * 85 | *********************************/ 86 | /** 87 | * This interface define the shape of your resolver 88 | * Note that this type is designed to be compatible with graphql-tools resolvers 89 | * However, you can still use other generated interfaces to make your resolver type-safed 90 | */ 91 | export interface Resolver { 92 | Mutation?: MutationTypeResolver; 93 | PublicUser?: PublicUserTypeResolver; 94 | Query?: QueryTypeResolver; 95 | Subscription?: SubscriptionTypeResolver; 96 | User?: UserTypeResolver; 97 | } 98 | export interface MutationTypeResolver { 99 | _empty?: MutationTo_emptyResolver; 100 | registerUser?: MutationToRegisterUserResolver; 101 | } 102 | 103 | export interface MutationTo_emptyResolver { 104 | (parent: TParent, args: {}, context: any, info: GraphQLResolveInfo): TResult; 105 | } 106 | 107 | export interface MutationToRegisterUserArgs { 108 | input: InputRegisterUser; 109 | } 110 | export interface MutationToRegisterUserResolver { 111 | (parent: TParent, args: MutationToRegisterUserArgs, context: any, info: GraphQLResolveInfo): TResult; 112 | } 113 | 114 | export interface PublicUserTypeResolver { 115 | id?: PublicUserToIdResolver; 116 | name?: PublicUserToNameResolver; 117 | email?: PublicUserToEmailResolver; 118 | } 119 | 120 | export interface PublicUserToIdResolver { 121 | (parent: TParent, args: {}, context: any, info: GraphQLResolveInfo): TResult; 122 | } 123 | 124 | export interface PublicUserToNameResolver { 125 | (parent: TParent, args: {}, context: any, info: GraphQLResolveInfo): TResult; 126 | } 127 | 128 | export interface PublicUserToEmailResolver { 129 | (parent: TParent, args: {}, context: any, info: GraphQLResolveInfo): TResult; 130 | } 131 | 132 | export interface QueryTypeResolver { 133 | _empty?: QueryTo_emptyResolver; 134 | loginUser?: QueryToLoginUserResolver; 135 | getUser?: QueryToGetUserResolver; 136 | } 137 | 138 | export interface QueryTo_emptyResolver { 139 | (parent: TParent, args: {}, context: any, info: GraphQLResolveInfo): TResult; 140 | } 141 | 142 | export interface QueryToLoginUserArgs { 143 | input: InputLogin; 144 | } 145 | export interface QueryToLoginUserResolver { 146 | (parent: TParent, args: QueryToLoginUserArgs, context: any, info: GraphQLResolveInfo): TResult; 147 | } 148 | 149 | export interface QueryToGetUserArgs { 150 | id: string; 151 | } 152 | export interface QueryToGetUserResolver { 153 | (parent: TParent, args: QueryToGetUserArgs, context: any, info: GraphQLResolveInfo): TResult; 154 | } 155 | 156 | export interface SubscriptionTypeResolver { 157 | _empty?: SubscriptionTo_emptyResolver; 158 | } 159 | 160 | export interface SubscriptionTo_emptyResolver { 161 | resolve?: (parent: TParent, args: {}, context: any, info: GraphQLResolveInfo) => TResult; 162 | subscribe: (parent: TParent, args: {}, context: any, info: GraphQLResolveInfo) => AsyncIterator; 163 | } 164 | 165 | export interface UserTypeResolver { 166 | id?: UserToIdResolver; 167 | name?: UserToNameResolver; 168 | email?: UserToEmailResolver; 169 | token?: UserToTokenResolver; 170 | } 171 | 172 | export interface UserToIdResolver { 173 | (parent: TParent, args: {}, context: any, info: GraphQLResolveInfo): TResult; 174 | } 175 | 176 | export interface UserToNameResolver { 177 | (parent: TParent, args: {}, context: any, info: GraphQLResolveInfo): TResult; 178 | } 179 | 180 | export interface UserToEmailResolver { 181 | (parent: TParent, args: {}, context: any, info: GraphQLResolveInfo): TResult; 182 | } 183 | 184 | export interface UserToTokenResolver { 185 | (parent: TParent, args: {}, context: any, info: GraphQLResolveInfo): TResult; 186 | } 187 | } 188 | } 189 | --------------------------------------------------------------------------------