{"id":21506535,"url":"https://github.com/khomsiadam/create-express-gql-ts","last_synced_at":"2025-07-16T01:33:32.206Z","repository":{"id":57685595,"uuid":"494066846","full_name":"KhomsiAdam/create-express-gql-ts","owner":"KhomsiAdam","description":"Set up and build a Node.js GraphQL API using Typescript, Express, Mongoose with a maintainable and scalable structure. If you prefer using REST: https://github.com/KhomsiAdam/create-express-rest-ts","archived":false,"fork":false,"pushed_at":"2022-05-29T13:17:05.000Z","size":406,"stargazers_count":7,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2024-04-28T23:42:59.091Z","etag":null,"topics":["access-token","apollo-server","codegen","dataloader","express","graphql","jest","jwt","mongoose","nodejs","refresh-token","typescript"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/create-express-gql-ts","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/KhomsiAdam.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2022-05-19T12:42:18.000Z","updated_at":"2022-11-04T15:25:25.000Z","dependencies_parsed_at":"2022-09-26T16:40:22.163Z","dependency_job_id":null,"html_url":"https://github.com/KhomsiAdam/create-express-gql-ts","commit_stats":null,"previous_names":[],"tags_count":3,"template":true,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KhomsiAdam%2Fcreate-express-gql-ts","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KhomsiAdam%2Fcreate-express-gql-ts/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KhomsiAdam%2Fcreate-express-gql-ts/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KhomsiAdam%2Fcreate-express-gql-ts/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/KhomsiAdam","download_url":"https://codeload.github.com/KhomsiAdam/create-express-gql-ts/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":226090716,"owners_count":17572117,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["access-token","apollo-server","codegen","dataloader","express","graphql","jest","jwt","mongoose","nodejs","refresh-token","typescript"],"created_at":"2024-11-23T19:41:14.447Z","updated_at":"2024-11-23T19:41:15.181Z","avatar_url":"https://github.com/KhomsiAdam.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\r\n  \u003cimg src=\"https://user-images.githubusercontent.com/9354045/169316293-a3185c08-960f-48ea-ae83-12df6c26582f.svg\" alt=\"Typescript Node Express GraphQL API\"\u003e\u003c/img\u003e\r\n\u003c/p\u003e\r\n\u003cp align=\"center\"\u003e\r\n  \u003ca href=\"LICENSE\"\u003e\r\n    \u003cimg src=\"https://img.shields.io/badge/license-MIT--Clause-brightgreen.svg?style=flat-square\" alt=\"Software License\"\u003e\u003c/img\u003e\r\n  \u003c/a\u003e\r\n  \u003ca href=\"https://github.com/KhomsiAdam/create-express-gql-ts/releases\"\u003e\r\n    \u003cimg src=\"https://img.shields.io/github/release/KhomsiAdam/create-express-gql-ts.svg?style=flat-square\" alt=\"Latest Version\"\u003e\u003c/img\u003e\r\n  \u003c/a\u003e\r\n\u003c/p\u003e\r\n  \r\n# Introduction\r\n\r\nCreate a maintainable and scalable Node.js GraphQL API with TypeScript, Express, Mongoose and Apollo Server.\r\n\r\nThe project structure is based on MVC and follows it's basic principles but is a little bit different in which instead of having the entities logic spread out into specific folders (models folder containing all models, controllers folder containing all controllers etc...).\r\n\r\nEach entity has it's own folder containing all it's core logic in isolation from other entities. Let's take the `User` entity as an example:\r\n\r\n```\r\nsrc\r\n└── entities\r\n    └── user\r\n        ├── constants.ts\r\n        ├── interface.ts\r\n        ├── model.ts\r\n        ├── permissions.ts\r\n        ├── resolvers.ts\r\n        ├── typeDefs.ts\r\n        └── validation.ts\r\n```\r\n\r\nWith this structure it is easier to maintain and scale with multiple entities (you will rarely have to switch between folders in order to manage one entity).\r\n\r\nThe project comes with many built-in features, such as:\r\n\r\n- Authentication with [JWT](https://www.npmjs.com/package/jsonwebtoken): providing both an access token and refresh token (sent as a secure http only cookie and saved in the database).\r\n- Unified login system for support of multiple roles of users.\r\n- Validation using [Joi](https://joi.dev/).\r\n- [Jest](https://jestjs.io/) for unit and integration testing.\r\n- Entity folder/files generation with a custom script.\r\n- [PM2](https://pm2.keymetrics.io/) as a process manager.\r\n- Seeding data examples.\r\n- Logger with [winston](https://www.npmjs.com/package/winston) and [morgan](https://www.npmjs.com/package/morgan).\r\n- Custom Error/Response handling.\r\n- Filtering, sorting, pagination.\r\n- [GraphQL Codegen](https://www.graphql-code-generator.com/) to generate typed queries, mutations, resolvers from schema.\r\n- [GraphQL Shield](https://www.graphql-shield.com/) to handle permissions and authorizations.\r\n- [GraphQL Dataloader](https://github.com/graphql/dataloader) as a layer for batching and caching data.\r\n- more details below...\r\n\r\n# Table of Contents\r\n\r\n\u003c!--ts--\u003e\r\n\r\n- [Setup](#setup)\r\n  - [Usage](#usage)\r\n  - [Configuration](#configuration)\r\n- [Directory Structure](#directory-structure)\r\n- [Scripts](#scripts)\r\n- [Features](#features)\r\n- [Contributions](#contributions)\r\n\u003c!--te--\u003e\r\n\r\n# Setup\r\n\r\n## Usage\r\n\r\nTo create a project, simply run:\r\n\r\n```bash\r\nnpx create-express-gql-ts my-app\r\n```\r\n\r\nor for a quick start if you are using vscode:\r\n\r\n```bash\r\nnpx create-express-gql-ts my-app\r\ncd my-app\r\ncode .\r\n```\r\n\r\n*By default, it uses `yarn` to install dependencies.\r\n\r\n- If you prefer another package manager you can pass it as an argument:\r\n\r\nfor `npm`:\r\n\r\n```bash\r\nnpx create-express-gql-ts my-app --npm\r\n```\r\nfor `pnpm`:\r\n\r\n```bash\r\nnpx create-express-gql-ts my-app --pnpm\r\n```\r\n\r\n*You can pass package manager specific arguments as flags as well after the package manager argument. As an example with `npm` you might need to pass in the `--force` flag to force installation even with conflicting peer dependencies:\r\n\r\n```bash\r\nnpx create-express-gql-ts my-app --npm --force\r\n```\r\n\r\nAlternatively, you can clone the repository (or download or use as a template):\r\n\r\n```bash\r\ngit clone https://github.com/KhomsiAdam/create-express-gql-ts.git\r\n```\r\n\r\nThen open the project folder and install the required dependencies:\r\n\r\n```bash\r\nyarn\r\n```\r\n\r\n*If you want to use another package manager after using this method instead of `npx`, before installing dependencies you should modify the `pre-commit` script in `.husky` to match your package manager of choice (then deleting the `yarn.lock` file if it would cause any conflicts).\r\n\r\n*In the `.github/yml` folder, there is a workflow file for each package manager. You can copy the file that matches your package manager into `.github/workflows` and delete `.github/workflows/yarn.yml`.\r\n\r\n\r\n[Back to top](#table-of-contents)\r\n\r\n## Configuration\r\n\r\nSetup your environment variables. In your root directory, you will find a `.env.example` file. Copy and/or rename to `.env` or:\r\n\r\n```\r\ncp .env.example .env\r\n```\r\n\r\nThen run the development server with the command below (depending on your package manager of choice):\r\n\r\n```bash\r\nyarn dev\r\n```\r\nor:\r\n```bash\r\nnpm run dev\r\n```\r\nor:\r\n```bash\r\npnpm dev\r\n```\r\n\r\nThe database should be connected and your server should be running at `http://localhost:${port}/graphql`. You can start testing and querying the API.\r\n\r\n[Back to top](#table-of-contents)\r\n\r\n# Directory Structure\r\n\r\n```\r\nsrc/\r\n├── __tests__/                  # Groups all your integration tests and the testing server\r\n├── config/                     # Apollo server, context, database and schema configuration\r\n├── entities/                   # Contains all entities (generated entities end up here with `yarn entity`)\r\n├── generated/                  # Typed queries, mutation resolvers... by GraphQL code generator\r\n├── helpers/                    # Any utility or helper functions/methods go here\r\n├── middlewares/                # Express \u0026 Apollo middlewares\r\n├── seeders/                    # Data seeders examples\r\n├── services/                   # Contains mostly global and reusable logic (such as auth and crud)\r\n├── tasks/                      # Scripts (contains the script to generate entities based of templates)\r\n│   └── templates/              # Contains entity templates (default and user type)\r\n├── types/                      # Custom/global type definitions\r\n└── index.ts                    # App entry point (initializes database connection and express server)\r\n```\r\n\r\n[Back to top](#table-of-contents)\r\n\r\n# Scripts\r\n\r\n*`yarn` can be replaced by `npm run` or `pnpm` depending on your preferred package manager.\r\n\r\n- Run compiled javascript production build (requires build):\r\n\r\n```bash\r\nyarn start\r\n```\r\n\r\n\u003chr\u003e\r\n\r\n- Run compiled javascript production build with pm2 in cluster mode (requires build):\r\n\r\n```bash\r\nyarn start:pm2\r\n```\r\n\r\n\u003chr\u003e\r\n\r\n- Compiles typescript into javascript and build your app:\r\n\r\n```bash\r\nyarn build\r\n```\r\n\r\n\u003chr\u003e\r\n\r\n- Run the typescript development build:\r\n\r\n```bash\r\nyarn dev\r\n```\r\n\r\n\u003chr\u003e\r\n\r\n- Run the typescript development build with the `--trace-sync-io` tag to detect any synchronous I/O:\r\n\r\n```bash\r\nyarn dev:sync\r\n```\r\n\r\n\u003chr\u003e\r\n\r\n- Run the typescript development build with PM2:\r\n\r\n```bash\r\nyarn dev:pm2\r\n```\r\n\r\n\u003chr\u003e\r\n\r\n- Seed an Admin:\r\n\r\n```bash\r\nyarn seed:admin\r\n```\r\n\r\n\u003chr\u003e\r\n\r\n- Seed fake users based on json data file:\r\n\r\n```bash\r\nyarn seed:users\r\n```\r\n\r\n\u003chr\u003e\r\n\r\n- Generate an entity based of either the `default` or `user` template (prompts for a template selection and entity name, then create it's folder under `src/entities`)\r\n\r\n```bash\r\nyarn entity\r\n```\r\n\r\n\\*Entities created have their constants, resolvers (with basic crud), permissions all automatically setup from the provided name. The interface, model, typeDefs and validation need to be filled with the needed fields.\r\n\r\n\u003chr\u003e\r\n\r\n- Eslint (lint, lint and fix):\r\n\r\n```bash\r\nyarn lint\r\n```\r\n\r\n```bash\r\nyarn lint:fix\r\n```\r\n\r\n\u003chr\u003e\r\n\r\n- Jest (all, unit, integration, coverage, watch, watchAll):\r\n\r\n```bash\r\nyarn test\r\n```\r\n\r\n```bash\r\nyarn test:unit\r\n```\r\n\r\n```bash\r\nyarn test:int\r\n```\r\n\r\n```bash\r\nyarn test:coverage\r\n```\r\n\r\n```bash\r\nyarn test:watch\r\n```\r\n\r\n```bash\r\nyarn test:watchAll\r\n```\r\n\r\n\u003chr\u003e\r\n\r\n- PM2 (kill, monit):\r\n\r\n```bash\r\nyarn kill\r\n```\r\n\r\n```bash\r\nyarn monit\r\n```\r\n\r\n\u003chr\u003e\r\n\r\n- GraphQL Code Generator:\r\n\r\n```bash\r\nyarn gen\r\n```\r\n\r\n```bash\r\nyarn gen:watch\r\n```\r\n\r\n\u003chr\u003e\r\n\r\n- Commitizen:\r\n\r\n```bash\r\nyarn cz\r\n```\r\n\r\n[Back to top](#table-of-contents)\r\n\r\n# Features\r\n\r\n## Entities\r\n\r\nlet's imagine we generated a `Post` entity with the `default` template `src/entities/post`:\r\n\r\n```\r\nsrc\r\n└── entities\r\n    └── post\r\n        ├── constants.ts\r\n        ├── interface.ts\r\n        ├── model.ts\r\n        ├── permissions.ts\r\n        ├── resolvers.ts\r\n        ├── typeDefs.ts\r\n        └── validation.ts\r\n```\r\n\r\nIt's constants, resolvers, permissions are all ready and setup:\r\n\r\n`src/entities/post/constants.ts`:\r\n\r\n```typescript\r\nexport enum SuccessMessages {\r\n  POST_CREATED = 'Post created successfully.',\r\n  POST_UPDATED = 'Post updated successfully.',\r\n  POST_DELETED = 'Post deleted successfully.',\r\n}\r\n\r\nexport enum ErrorMessages {\r\n  POSTS_NOT_FOUND = 'No posts found.',\r\n  POST_NOT_FOUND = 'Post was not found.',\r\n}\r\n```\r\n\r\n`src/entities/post/resolvers.ts`:\r\n\r\n```typescript\r\nimport * as resolver from '@services/crud.service';\r\nimport type {\r\n  Resolvers,\r\n  PostResult,\r\n  PostsResult,\r\n  PostCreatedResult,\r\n  PostUpdatedResult,\r\n  PostRemovedResult,\r\n} from '@generated/types';\r\nimport { PostModel } from './model';\r\nimport { ErrorMessages, SuccessMessages } from './constants';\r\nimport { createPostSchema, updatePostSchema } from './validation';\r\n\r\nexport const resolvers: Resolvers = {\r\n  Query: {\r\n    getAllPosts: async (_parent, args): Promise\u003cPostsResult\u003e =\u003e\r\n      resolver.getAll(PostModel, args, ErrorMessages.POSTS_NOT_FOUND, 'Posts', 'PostNotFound'),\r\n    getPostById: async (_parent, args): Promise\u003cPostResult\u003e =\u003e\r\n      resolver.getById(PostModel, args.id, ErrorMessages.POST_NOT_FOUND, 'PostBy', 'PostNotFound'),\r\n    getPostByField: async (_parent, args): Promise\u003cPostResult\u003e =\u003e\r\n      resolver.getByField(PostModel, args.field, args.value, ErrorMessages.POST_NOT_FOUND, 'PostBy', 'PostNotFound'),\r\n  },\r\n\r\n  Mutation: {\r\n    createPost: async (_parent, args): Promise\u003cPostCreatedResult\u003e =\u003e\r\n      resolver.create(\r\n        PostModel,\r\n        args.input,\r\n        createPostSchema,\r\n        SuccessMessages.POST_CREATED,\r\n        'PostCreated',\r\n        'PostNotFound',\r\n      ),\r\n    updatePost: async (_parent, args): Promise\u003cPostUpdatedResult\u003e =\u003e\r\n      resolver.update(\r\n        PostModel,\r\n        args.id,\r\n        args.input,\r\n        updatePostSchema,\r\n        SuccessMessages.POST_UPDATED,\r\n        ErrorMessages.POST_NOT_FOUND,\r\n        'PostUpdated',\r\n        'PostNotFound',\r\n      ),\r\n    removePost: async (_parent, args): Promise\u003cPostRemovedResult\u003e =\u003e\r\n      resolver.remove(\r\n        PostModel,\r\n        args.id,\r\n        SuccessMessages.POST_DELETED,\r\n        ErrorMessages.POST_NOT_FOUND,\r\n        'PostRemoved',\r\n        'PostNotFound',\r\n      ),\r\n  },\r\n};\r\n```\r\n\r\n`src/entities/post/typeDefs.ts`:\r\n\r\n```typescript\r\nimport { gql } from 'apollo-server-express';\r\n\r\nexport const typeDefs = gql`\r\n# Types\r\n  type Post {\r\n    _id: ObjectId\r\n    # Add your fields here #\r\n    createdAt: DateTime\r\n    updatedAt: DateTime\r\n  }\r\n  ## Post by id/field\r\n  type PostBy {\r\n    entity: Post!\r\n  }\r\n  ## All Posts\r\n  type Posts {\r\n    entities: [Post!]!\r\n  }\r\n  ## Created Post\r\n  type PostCreated {\r\n    entity: Post!\r\n    message: String!\r\n  }\r\n  ## Updated Post\r\n  type PostUpdated {\r\n    entity: Post!\r\n    message: String!\r\n  }\r\n  ## Removed Post\r\n  type PostRemoved {\r\n    entity: Post!\r\n    message: String!\r\n  }\r\n  ## Not found\r\n  type PostNotFound {\r\n    message: String!\r\n  }\r\n\r\n  # Inputs\r\n  input PostCreatedInput {\r\n    # Add your fields here #\r\n  }\r\n  input PostUpdatedInput {\r\n    # Add your fields here #\r\n  }\r\n\r\n  # Unions\r\n  union PostResult = PostBy | PostNotFound\r\n  union PostsResult = Posts | PostNotFound\r\n  union PostCreatedResult = PostCreated | PostNotFound\r\n  union PostUpdatedResult = PostUpdated | PostNotFound\r\n  union PostRemovedResult = PostRemoved | PostNotFound\r\n\r\n  # Queries\r\n  type Query {\r\n    getAllPosts(sort: SortInput, filter: FilterInput, paginate: PaginationInput): PostsResult!\r\n    getPostById(id: ObjectId!): PostResult!\r\n    getPostByField(field: String!, value: String!): PostResult!\r\n  }\r\n\r\n  # Mutations\r\n  type Mutation {\r\n    createPost(input: PostCreatedInput!): PostCreatedResult!\r\n    updatePost(id: ObjectId!, input: PostUpdatedInput!): PostUpdatedResult!\r\n    removePost(id: ObjectId!): PostRemovedResult!\r\n  }\r\n`;\r\n```\r\n\r\n\\*After generating your entity, you should complete the definitions by adding your fiels under the main type and for the create and update inputs. For each operation the type of data we could get as a result is defined using an union type.\r\n\r\n`src/entities/post/permissions.ts`:\r\n\r\n```typescript\r\nimport { is } from '@middlewares/rules';\r\nimport { or } from 'graphql-shield';\r\n\r\nconst permissions = {\r\n  Query: {\r\n    getAllPosts: is.Auth,\r\n    getPostById: is.Auth,\r\n    getPostByField: is.Auth,\r\n  },\r\n  Mutation: {\r\n    createPost: is.Auth,\r\n    updatePost: or(is.Own, is.Admin),\r\n    removePost: or(is.Own, is.Admin),\r\n  },\r\n};\r\n\r\nexport default permissions;\r\n```\r\n\r\n\\*Most operations by default have the `is.Auth` middleware that require a user to be authenticated to access them, you can either omit it if you want an operation to be public or use the `allow` rule from `graphql-shield`. You can specify which user role is allowed (`is.Admin` or `is.User`) and also use operators such as `or`, `and`.\r\n\r\n`src/middlewares/rules.ts`:\r\n\r\n```typescript\r\nimport { rule } from 'graphql-shield';\r\nimport { IRuleConstructorOptions } from 'graphql-shield/dist/types';\r\nimport { verifyAuth } from '@services/auth.service';\r\nimport { Roles, Permissions } from '@entities/auth/constants';\r\n\r\nconst options: IRuleConstructorOptions = { cache: 'contextual' };\r\n\r\nexport const is = {\r\n  Auth: rule(options)(async (_parent, _args, context) =\u003e verifyAuth(context.req)),\r\n  Self: rule(options)(async (_parent, args, context) =\u003e verifyAuth(context.req, '', Permissions.SELF, args.id)),\r\n  Own: rule(options)(async (_parent, args, context) =\u003e verifyAuth(context.req, '', Permissions.OWN, args.id)),\r\n  User: rule(options)(async (_parent, _args, context) =\u003e verifyAuth(context.req, Roles.USER)),\r\n  Admin: rule(options)(async (_parent, _args, context) =\u003e verifyAuth(context.req, Roles.ADMIN)),\r\n};\r\n```\r\n\r\n*The `is.Self` is for a user to operate resolvers that are targeted at himself.\r\n*The `is.Own` is for a user to handle an entity he owns so that no other user can operate it with the help of:\r\n\r\n`src/helpers/getEntityFromOperation.ts`:\r\n\r\n```typescript\r\n// Get entity name from graphql operation (query/mutation) to dynamically query data related to that entity\r\nexport const getEntityFromOperation = (entities: Array\u003cstring\u003e, operation: string) =\u003e\r\n  entities.find((entity) =\u003e operation.includes(entity));\r\n```\r\n\r\n\\*It is required for operations to be properly named as it is good practice. But it is also used here to get the name of the entity: `const entityName = getEntityFromOperation(modelNames(), req.body.operationName);`. `modelNames()` from `mongoose` gets us all the entity names in our database. `req.body.operationName` gets us the name of the operation requestes for example: `UpdatePost`. The method will return `Post` as the entity name so we can find if the entity requested is owned by the user performing the operation.\r\n\r\nThe interface, model and validation will have to be filled by the needed fields much like the typeDefs.\r\n\r\n`src/entities/post/interface.ts`:\r\n\r\n```typescript\r\nexport interface PostEntity {}\r\n```\r\n\r\n`src/entities/post/model.ts`:\r\n\r\n```typescript\r\nimport { Schema, model } from 'mongoose';\r\n\r\nimport { PostEntity } from './interface';\r\n\r\nconst PostSchema = new Schema\u003cPostEntity\u003e({}, { timestamps: true });\r\n\r\nexport const PostModel = model\u003cPostEntity\u003e('Post', PostSchema);\r\n```\r\n\r\n`src/entities/post/validation.ts`:\r\n\r\n```typescript\r\nimport Joi from 'joi';\r\n\r\nexport const createPostSchema = Joi.object({});\r\n\r\nexport const updatePostSchema = Joi.object({});\r\n```\r\n\r\nThe `user` entity template slightly differs from the default one as it is destined for another type of user (another role for example).\r\n\r\nUsing:\r\n\r\n```bash\r\nyarn entity\r\n```\r\n```bash\r\nnpm run entity\r\n```\r\n```bash\r\npnpm entity\r\n```\r\n\r\nLet's create a `Manager` entity with the `user` template `src/entities/manager`.\r\n\r\n`src/entities/manager/constants.ts`:\r\n\r\n```typescript\r\nexport enum SuccessMessages {\r\n  MANAGER_UPDATED = 'Manager updated successfully.',\r\n  MANAGER_DELETED = 'Manager deleted successfully.',\r\n}\r\n\r\nexport enum ErrorMessages {\r\n  MANAGERS_NOT_FOUND = 'No managers found.',\r\n  MANAGER_NOT_FOUND = 'Manager was not found.',\r\n}\r\n\r\nexport const SALT_ROUNDS = 12;\r\n```\r\n\r\n`src/entities/manager/interface.ts`:\r\n\r\n```typescript\r\nimport { Types } from 'mongoose';\r\n\r\nexport interface ManagerEntity {\r\n  email: string;\r\n  password: string;\r\n  firstname: string;\r\n  lastname: string;\r\n  role?: Types.ObjectId;\r\n}\r\n```\r\n\r\n`src/entities/manager/model.ts`:\r\n\r\n```typescript\r\nimport { Schema, model } from 'mongoose';\r\nimport { genSalt as bcryptGenSalt, hash as bcryptHash } from 'bcryptjs';\r\n\r\nimport { AuthModel } from '@entities/auth/model';\r\nimport type { ManagerEntity } from './interface';\r\nimport { SALT_ROUNDS } from './constants';\r\n\r\nconst ManagerSchema = new Schema\u003cManagerEntity\u003e(\r\n  {\r\n    email: {\r\n      type: String,\r\n      required: true,\r\n      unique: true,\r\n    },\r\n    password: {\r\n      type: String,\r\n      required: true,\r\n      select: false,\r\n    },\r\n    firstname: {\r\n      type: String,\r\n      required: true,\r\n    },\r\n    lastname: {\r\n      type: String,\r\n      required: true,\r\n    },\r\n    role: {\r\n      type: Schema.Types.ObjectId,\r\n      ref: 'Auth',\r\n    },\r\n  },\r\n  { timestamps: true },\r\n);\r\n\r\n// Before creating a manager\r\nManagerSchema.pre('save', async function save(next) {\r\n  // Only hash password if it has been modified or new\r\n  if (!this.isModified('password')) return next();\r\n  // Generate salt and hash password\r\n  const salt = await bcryptGenSalt(SALT_ROUNDS);\r\n  this.password = await bcryptHash(this.password, salt);\r\n  next();\r\n});\r\n// After creating a manager\r\nManagerSchema.post('save', async (doc) =\u003e {\r\n  // Create manager in auth collection\r\n  await AuthModel.create({ email: doc.email, role: 'Manager' });\r\n});\r\nManagerSchema.post('findOneAndDelete', async (doc) =\u003e {\r\n  // Delete manager from auth collection\r\n  await AuthModel.deleteOne({ email: doc.email });\r\n});\r\n\r\nexport const ManagerModel = model\u003cManagerEntity\u003e('Manager', ManagerSchema);\r\n```\r\n\r\n`src/entities/manager/validation.ts`:\r\n\r\n```typescript\r\nimport Joi from 'joi';\r\n\r\nexport const managerSchema = Joi.object({\r\n  firstname: Joi.string().trim(),\r\n  lastname: Joi.string().trim(),\r\n});\r\n```\r\n\r\n`src/entities/manager/permissions.ts`:\r\n\r\n```typescript\r\nimport { is } from '@middlewares/rules';\r\nimport { or } from 'graphql-shield';\r\n\r\nconst permissions = {\r\n  Query: {\r\n    getAllManagers: is.Auth,\r\n    getManagerById: is.Auth,\r\n    getManagerByField: is.Auth,\r\n  },\r\n  Mutation: {\r\n    updateManager: or(is.Self, is.Admin),\r\n    removeManager: or(is.Self, is.Admin),\r\n  },\r\n};\r\n\r\nexport default permissions;\r\n```\r\n\r\n`src/entities/manager/resolvers.ts`:\r\n\r\n```typescript\r\nimport * as resolver from '@services/crud.service';\r\nimport type {\r\n  Resolvers,\r\n  ManagerResult,\r\n  ManagersResult,\r\n  ManagerUpdatedResult,\r\n  ManagerRemovedResult,\r\n} from '@generated/types';\r\nimport type { AuthData } from '@entities/auth/interface';\r\nimport { ManagerModel } from './model';\r\nimport { ErrorMessages, SuccessMessages } from './constants';\r\nimport { managerSchema } from './validation';\r\n\r\nexport const resolvers: Resolvers = {\r\n  Query: {\r\n    getAllManagers: async (_parent, args): Promise\u003cManagersResult\u003e =\u003e\r\n      resolver.getAll(ManagerModel, args, ErrorMessages.MANAGERS_NOT_FOUND, 'Managers', 'ManagerNotFound'),\r\n    getManagerById: async (_parent, args): Promise\u003cManagerResult\u003e =\u003e\r\n      resolver.getById(ManagerModel, args.id, ErrorMessages.MANAGER_NOT_FOUND, 'ManagerBy', 'ManagerNotFound'),\r\n    getManagerByField: async (_parent, args): Promise\u003cManagerResult\u003e =\u003e\r\n      resolver.getByField(\r\n        ManagerModel,\r\n        args.field,\r\n        args.value,\r\n        ErrorMessages.MANAGER_NOT_FOUND,\r\n        'ManagerBy',\r\n        'ManagerNotFound',\r\n      ),\r\n  },\r\n\r\n  Mutation: {\r\n    updateManager: async (_parent, args): Promise\u003cManagerUpdatedResult\u003e =\u003e\r\n      resolver.update(\r\n        ManagerModel,\r\n        args.id,\r\n        args.input,\r\n        managerSchema,\r\n        SuccessMessages.MANAGER_UPDATED,\r\n        ErrorMessages.MANAGER_NOT_FOUND,\r\n        'ManagerUpdated',\r\n        'ManagerNotFound',\r\n      ),\r\n    removeManager: async (_parent, args): Promise\u003cManagerRemovedResult\u003e =\u003e\r\n      resolver.remove(\r\n        ManagerModel,\r\n        args.id,\r\n        SuccessMessages.MANAGER_DELETED,\r\n        ErrorMessages.MANAGER_NOT_FOUND,\r\n        'ManagerRemoved',\r\n        'ManagerNotFound',\r\n      ),\r\n  },\r\n\r\n  Manager: {\r\n    role: async ({ role }, _args, { dataloader }): Promise\u003cAuthData\u003e =\u003e dataloader.auth.load(role),\r\n  },\r\n};\r\n```\r\n\r\n\\*[GraphQL Dataloader](https://github.com/graphql/dataloader) is used instead of relying on `.populate()` and offers better performance through batching and caching. After creating a new entity you should add it's own dataloader under `src/middlewares/loader.ts` (like below if we created a `Manager` and `Post` entities as examples):\r\n\r\n```typescript\r\nimport DataLoader from 'dataloader';\r\nimport type { Model } from 'mongoose';\r\nimport { AuthModel } from '@entities/auth/model';\r\nimport { AdminModel } from '@entities/admin/model';\r\nimport { UserModel } from '@entities/user/model';\r\nimport { ManagerModel } from '@entities/manager/model';\r\nimport { PostModel } from '@entities/post/model';\r\n\r\n// Create a dataloader for the given model\r\nexport const createLoader = (entityModel: Model\u003cany\u003e) =\u003e {\r\n  const loader = new DataLoader(async (keys) =\u003e {\r\n    const data = await entityModel.find({ _id: { $in: keys } });\r\n    return keys.map((key) =\u003e data.find((item) =\u003e item._id.equals(key)));\r\n  });\r\n  return {\r\n    load: async (id: unknown) =\u003e (id ? loader.load(id) : null),\r\n    loadMany: async (ids: ArrayLike\u003cunknown\u003e) =\u003e loader.loadMany(ids),\r\n    clear: (id: unknown) =\u003e loader.clear(id),\r\n    clearAll: () =\u003e loader.clearAll(),\r\n  };\r\n};\r\n\r\n// Add dataloader entry for each newly created Model\r\nexport const dataloader = {\r\n  auth: createLoader(AuthModel),\r\n  admin: createLoader(AdminModel),\r\n  user: createLoader(UserModel),\r\n  manager: createLoader(ManagerModel),\r\n  post: createLoader(PostModel),\r\n};\r\n```\r\n\r\n`src/entities/manager/typeDefs.ts`\r\n\r\n```typescript\r\nimport { gql } from 'apollo-server-express';\r\n\r\nexport const typeDefs = gql`\r\n  # Types\r\n  type Manager {\r\n    _id: ObjectId\r\n    firstname: String\r\n    lastname: String\r\n    email: String\r\n    role: Auth\r\n    createdAt: DateTime\r\n    updatedAt: DateTime\r\n  }\r\n  ## Manager by id/field\r\n  type ManagerBy {\r\n    entity: Manager!\r\n  }\r\n  ## All Managers\r\n  type Managers {\r\n    entities: [Manager!]!\r\n  }\r\n  ## Updated Manager\r\n  type ManagerUpdated {\r\n    entity: Manager!\r\n    message: String!\r\n  }\r\n  ## Removed Manager\r\n  type ManagerRemoved {\r\n    entity: Manager!\r\n    message: String!\r\n  }\r\n  ## Not found\r\n  type ManagerNotFound {\r\n    message: String!\r\n  }\r\n\r\n  # Inputs\r\n  input ManagerUpdatedInput {\r\n    firstname: String\r\n    lastname: String\r\n  }\r\n\r\n  # Unions\r\n  union ManagerResult = ManagerBy | ManagerNotFound\r\n  union ManagersResult = Managers | ManagerNotFound\r\n  union ManagerUpdatedResult = ManagerUpdated | ManagerNotFound\r\n  union ManagerRemovedResult = ManagerRemoved | ManagerNotFound\r\n\r\n  # Queries\r\n  type Query {\r\n    getAllManagers(sort: SortInput, filter: FilterInput, paginate: PaginationInput): ManagersResult!\r\n    getManagerById(id: ObjectId!): ManagerResult!\r\n    getManagerByField(field: String!, value: String!): ManagerResult!\r\n  }\r\n\r\n  # Mutations\r\n  type Mutation {\r\n    updateManager(id: ObjectId!, input: ManagerUpdatedInput!): ManagerUpdatedResult!\r\n    removeManager(id: ObjectId!): ManagerRemovedResult!\r\n  }\r\n`;\r\n```\r\n\r\nThe `Manager` role should be added to the `Roles` constant `src/entities/auth/constants.ts`:\r\n\r\n```typescript\r\nexport enum Roles {\r\n  ADMIN = 'Admin',\r\n  USER = 'User',\r\n  MANAGER = 'Manager',\r\n}\r\n```\r\n\r\n\\*It automatically get added into the `src/entities/auth/interface.ts` and `src/entities/auth/model.ts`.\r\n\r\nThen optionally add another middleware `is.Manager` to check if user has a `Manager` role at `src/middlewares/rules.ts`:\r\n\r\n```typescript\r\nimport { rule } from 'graphql-shield';\r\nimport { IRuleConstructorOptions } from 'graphql-shield/dist/types';\r\nimport { verifyAuth } from '@services/auth.service';\r\nimport { Roles, Permissions } from '@entities/auth/constants';\r\n\r\nconst options: IRuleConstructorOptions = { cache: 'contextual' };\r\n\r\nexport const is = {\r\n  Auth: rule(options)(async (_parent, _args, context) =\u003e verifyAuth(context.req)),\r\n  Self: rule(options)(async (_parent, args, context) =\u003e verifyAuth(context.req, '', Permissions.SELF, args.id)),\r\n  Own: rule(options)(async (_parent, args, context) =\u003e verifyAuth(context.req, '', Permissions.OWN, args.id)),\r\n  Admin: rule(options)(async (_parent, _args, context) =\u003e verifyAuth(context.req, Roles.ADMIN)),\r\n  User: rule(options)(async (_parent, _args, context) =\u003e verifyAuth(context.req, Roles.USER)),\r\n  Manager: rule(options)(async (_parent, _args, context) =\u003e verifyAuth(context.req, Roles.MANAGER)),\r\n};\r\n```\r\n\r\nNow to create a user with a specified role, just send the role needed as part of the request body, it will automatically check if that role exists, if not the register will fail.\r\n\r\n\\*By default, registering creates user with a `User` role, and you cannot create a user with an `Admin` role with regular registering.\r\n\r\n## Error \u0026 Response Handling\r\n\r\nGraphQL handles responses and errors differently compared to REST. For example, if GraphQL doesn't find an entity, it will return `null` with a HTTP status code of 200. That's not very useful. Also this isn't really considered an error but just another type of response we could get. So we define under `src/entities/${entity}/typeDefs.ts` all possible responses and the type of data we could get as part of the schema using [union types](https://www.apollographql.com/docs/apollo-server/schema/unions-interfaces/).\r\n\r\n`src/helpers/CustomError.ts`:\r\n\r\n```typescript\r\nimport { ApolloError } from 'apollo-server-errors';\r\n\r\n// Custom error Apollo class\r\nexport class CustomError extends ApolloError {\r\n  constructor(message: string, statusCode: string) {\r\n    super(message, statusCode);\r\n    Object.defineProperty(this, 'name', { value: 'CustomError' });\r\n  }\r\n}\r\n\r\n// Custom Apollo error status codes\r\nexport enum StatusCode {\r\n  InvalidOperationName = 'INVALID_OPERATION_NAME',\r\n  JsonWebTokenError = 'JWT_INVALID_TOKEN',\r\n  SyntaxError = 'JWT_INVALID_SYNTAX',\r\n  ExpiredToken = 'JWT_EXPIRED_TOKEN',\r\n  SignatureError = 'JWT_INVALID_SIGNATURE',\r\n  InvalidAlgorithm = 'JWT_INVALID_ALGORITHM',\r\n}\r\n```\r\n\r\n\\*This can be used to return custom apollo errors with a custom status code. You can return or throw already defined [apollo errors](https://www.apollographql.com/docs/apollo-server/data/errors/#error-codes) using their generic `ApolloError` class and/or it's subclasses.\r\n\r\n```typescript\r\nimport { Types } from 'mongoose';\r\n\r\n// Custom responses for GraphQL resolvers to match the different returned types\r\nexport const customResponse = {\r\n  auth: (typeName: any, generatedToken: string, returnedRole: Types.ObjectId | string, resultMessage: string) =\u003e ({\r\n    __typename: typeName,\r\n    token: generatedToken,\r\n    role: returnedRole,\r\n    message: resultMessage,\r\n  }),\r\n  entities: (typeName: any, data: Array\u003cobject\u003e, resultMessage = '') =\u003e ({\r\n    __typename: typeName,\r\n    entities: data,\r\n    ...(resultMessage !== '' \u0026\u0026 { message: resultMessage }),\r\n  }),\r\n  entity: (typeName: any, data: object) =\u003e ({\r\n    __typename: typeName,\r\n    entity: data,\r\n  }),\r\n  operation: (typeName: any, data: object, resultMessage: string) =\u003e ({\r\n    __typename: typeName,\r\n    entity: data,\r\n    message: resultMessage,\r\n  }),\r\n  message: (typeName: any, resultMessage: string) =\u003e ({\r\n    __typename: typeName,\r\n    message: resultMessage,\r\n  }),\r\n};\r\n```\r\n\r\n\\*When running in development mode, the error response contains the message but also the error stack.\r\n\r\n## Validation\r\n\r\nData is validated using [Joi](https://joi.dev/). Check the [documentation](https://joi.dev/api/) for more details on how to write Joi validation schemas.\r\n\r\nThe validation schemas are defined in the folder for each entity. Let's take the `User` entity as an example so it would be in: `src/entities/user/validation.ts`:\r\n\r\n## Logging\r\n\r\nImport the logger from `src/services/logger.service.ts`. It is using the [winston](https://github.com/winstonjs/winston) logging library.\r\n\r\nLogging should be done according to the following severity levels (ascending order from most important to least important):\r\n\r\n```typescript\r\nimport { log } from '@services/logger.service';\r\nlog.error('error'); // level 0\r\nlog.warn('warning'); // level 1\r\nlog.info('information'); // level 2\r\nlog.http('http'); // level 3\r\nlog.debug('debug'); // level 4\r\n```\r\n\r\nIn development mode, log messages of all severity levels will be printed to the console.\r\n\r\nGraphQL operations are logged as HTTP requests using [morgan](https://github.com/expressjs/morgan): `src/middlewares/morgan.ts`\r\n\r\n## WIP:\r\n\r\n- Reset, forgot password.\r\n- Email service.\r\n- File upload.\r\n\r\n[Back to top](#table-of-contents)\r\n\r\n# Contributions\r\n\r\nContributions are welcome. To discuss any bugs, problems, fixes or improvements please refer to the [discussions](https://github.com/KhomsiAdam/create-express-ts-rest-api/discussions) section.\r\n\r\nBefore creating a pull request, make sure to open an [issue](https://github.com/KhomsiAdam/create-express-ts-rest-api/issues) first.\r\n\r\nCommitting your changes, fixes or improvements in a new branch with documentation will be appreciated.\r\n\r\n## License\r\n\r\n[MIT](LICENSE)\r\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkhomsiadam%2Fcreate-express-gql-ts","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkhomsiadam%2Fcreate-express-gql-ts","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkhomsiadam%2Fcreate-express-gql-ts/lists"}