├── .babelrc ├── .editorconfig ├── .env.example ├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .gitignore ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── nodemon.json ├── package.json └── src ├── apollo ├── apollo.js ├── config │ ├── context.js │ ├── formatError.js │ ├── formatResponse.js │ ├── index.js │ └── playground.js ├── data-sources │ ├── ExampleAPI │ │ ├── ExampleAPI.js │ │ └── index.js │ └── index.js ├── index.js ├── resolvers │ ├── ExampleResolver │ │ ├── ExampleResolver.js │ │ └── index.js │ └── index.js └── schema │ └── schema.graphql ├── config ├── apollo.js ├── index.js └── lib │ ├── index.js │ └── utils.js ├── express ├── app.js ├── controllers │ ├── API │ │ ├── API.js │ │ └── index.js │ └── index.js ├── index.js ├── middleware │ ├── captureErrors.js │ ├── errors.js │ ├── ignoreFavicon.js │ ├── index.js │ ├── logger.js │ └── notFound.js └── routes │ ├── api.js │ └── index.js └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "node": "current" 8 | } 9 | } 10 | ] 11 | ], 12 | "plugins": [ 13 | "@babel/plugin-proposal-export-default-from", 14 | "@babel/plugin-proposal-export-namespace-from", 15 | "@babel/plugin-proposal-optional-chaining", 16 | "import-graphql" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [Makefile] 15 | indent_size = 4 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | PORT=3001 2 | APOLLO_ENABLE_PLAYGROUND=true 3 | APOLLO_ENABLE_INTROSPECTION=true 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | __tests__ 2 | .babel-cache 3 | .vscode 4 | build 5 | coverage 6 | dist 7 | node_modules 8 | package-lock.json 9 | _refactor 10 | cache.js 11 | refactor-utils.js 12 | test/ 13 | jest-global-setup.js 14 | redis.js 15 | src/utils/validation/validation.js 16 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const ERROR = 2; 2 | const WARN = 1; 3 | const OFF = 0; 4 | 5 | module.exports = { 6 | root: true, 7 | env: { 8 | es6: true, 9 | node: true, 10 | jest: true, 11 | }, 12 | extends: [ 13 | 'airbnb-base', 14 | 'plugin:jest/recommended', 15 | 'eslint:recommended', 16 | 'plugin:import/errors', 17 | 'plugin:import/warnings', 18 | 'plugin:node/recommended', 19 | 'plugin:security/recommended', 20 | 'prettier', 21 | ], 22 | plugins: ['node', 'security', 'prettier', 'jest'], 23 | globals: { 24 | Atomics: 'readonly', 25 | SharedArrayBuffer: 'readonly', 26 | }, 27 | parser: 'babel-eslint', 28 | parserOptions: { 29 | ecmaVersion: 11, 30 | sourceType: 'module', 31 | }, 32 | rules: { 33 | 'prettier/prettier': ERROR, 34 | 'class-methods-use-this': [ERROR, { exceptMethods: ['willSendRequest'] }], 35 | 'func-names': ERROR, 36 | 'function-paren-newline': OFF, 37 | 'global-require': OFF, 38 | 'import/no-unresolved': OFF, 39 | 'max-len': ERROR, 40 | 'no-param-reassign': [ERROR, { props: false }], 41 | 'no-underscore-dangle': OFF, 42 | camelcase: OFF, 43 | 'no-unused-vars': [ERROR, { skipShapeProps: true }], 44 | 'node/no-unsupported-features/es-syntax': [ERROR, { ignores: ['modules'] }], 45 | 'security/detect-object-injection': OFF, 46 | 'no-console': OFF, 47 | 'security/detect-non-literal-fs-filename': OFF, 48 | 'jest/no-commented-out-tests': OFF, 49 | }, 50 | overrides: [ 51 | { 52 | files: ['./src/config/index.js'], 53 | rules: { 54 | 'max-len': OFF, 55 | }, 56 | }, 57 | { 58 | files: ['./src/test/**/*.js'], 59 | rules: { 60 | 'import/no-extraneous-dependencies': OFF, 61 | 'node/no-unpublished-import': OFF, 62 | }, 63 | }, 64 | ], 65 | }; 66 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Automatically normalize line endings for all text-based files 2 | # https://git-scm.com/docs/gitattributes#_end_of_line_conversion 3 | 4 | * text=auto 5 | 6 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 7 | 8 | # For the following file types, normalize line endings to LF on 9 | # checkin and prevent conversion to CRLF when they are checked out 10 | # (this is required in order to prevent newline related issues like, 11 | # for example, after the build script is run) 12 | 13 | .* text eol=lf 14 | *.css text eol=lf 15 | *.html text eol=lf 16 | *.js text eol=lf 17 | *.json text eol=lf 18 | *.md text eol=lf 19 | *.sh text eol=lf 20 | *.txt text eol=lf 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # MISC 64 | dist 65 | .env* 66 | !.env.ci 67 | !.env*.example 68 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 14 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .babel-cache 2 | .env 3 | .vscode 4 | build 5 | coverage 6 | dist 7 | node_modules 8 | package-lock.json 9 | package.json 10 | README.md 11 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | arrowParens: always 2 | bracketSpacing: true 3 | printWidth: 100 4 | semi: true 5 | singleQuote: true 6 | tabWidth: 2 7 | trailingComma: all 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright 2021 Justin Woodward 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Starter Node.js GraphQL server 2 | 3 | ## Quick Start 4 | 5 | CD into the project root and then Copy/paste the following into your CLI: 6 | 7 | ```sh 8 | cp .env.example .env 9 | npm install 10 | npm run start 11 | ``` 12 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "events": { 3 | "start": "npm run update-graphql-imports && clear && printf '\\e[3J'" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bare-nodejs-server", 3 | "version": "0.0.1", 4 | "description": "Starter Node.js GraphQL server", 5 | "private": true, 6 | "main": "src/start.js", 7 | "scripts": { 8 | "start": "npm run update-graphql-imports && nodemon -e js,graphql --exec babel-node -r dotenv/config -- ./src/index.js", 9 | "build": "babel src --quiet -d dist --ignore .test.js", 10 | "serve": "npm run build && node -r dotenv/config -- dist", 11 | "lint": "eslint ./src --ext .js --fix", 12 | "format": "prettier --write src/**/*.js", 13 | "update-graphql-imports": "rm -rf ./node_modules/.cache/@babel" 14 | }, 15 | "engines": { 16 | "node": ">=14.0.0" 17 | }, 18 | "author": "Justin Woodward ", 19 | "license": "MIT", 20 | "dependencies": { 21 | "@hapi/boom": "^9.1.0", 22 | "apollo-datasource-rest": "^0.9.7", 23 | "apollo-server-express": "^2.18.2", 24 | "body-parser": "^1.19.0", 25 | "cookie-parser": "^1.4.5", 26 | "dotenv": "^8.2.0", 27 | "express": "^4.16.4", 28 | "graphql": "^15.3.0", 29 | "graphql-tag": "^2.11.0", 30 | "morgan": "^1.10.0", 31 | "uuid": "^8.3.1" 32 | }, 33 | "devDependencies": { 34 | "@babel/cli": "^7.10.3", 35 | "@babel/core": "^7.10.3", 36 | "@babel/node": "^7.10.3", 37 | "@babel/plugin-proposal-export-default-from": "^7.10.1", 38 | "@babel/plugin-proposal-export-namespace-from": "^7.10.1", 39 | "@babel/plugin-proposal-optional-chaining": "^7.10.3", 40 | "@babel/preset-env": "^7.10.3", 41 | "babel-eslint": "^10.1.0", 42 | "babel-plugin-import-graphql": "^2.7.0", 43 | "eslint": "^6.8.0", 44 | "eslint-config-airbnb-base": "^14.2.0", 45 | "eslint-config-prettier": "^6.11.0", 46 | "eslint-plugin-import": "^2.21.2", 47 | "eslint-plugin-jest": "^23.17.1", 48 | "eslint-plugin-node": "^11.1.0", 49 | "eslint-plugin-prettier": "^3.1.4", 50 | "eslint-plugin-security": "^1.4.0", 51 | "husky": "^4.2.5", 52 | "jest": "^26.1.0", 53 | "jest-extended": "^0.11.5", 54 | "lint-staged": "^10.2.11", 55 | "nodemon": "^2.0.4", 56 | "prettier": "^2.0.5" 57 | }, 58 | "husky": { 59 | "hooks": { 60 | "pre-commit": "lint-staged" 61 | } 62 | }, 63 | "lint-staged": { 64 | "*.js,*.md,*.json": [ 65 | "eslint --fix", 66 | "prettier --write" 67 | ] 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/apollo/apollo.js: -------------------------------------------------------------------------------- 1 | import { ApolloServer } from 'apollo-server-express'; 2 | import typeDefs from './schema/schema.graphql'; 3 | import resolvers from './resolvers'; 4 | import dataSources from './data-sources'; 5 | import { introspection } from '../config'; 6 | import { formatError, context, playground } from './config'; 7 | 8 | const server = new ApolloServer({ 9 | context, 10 | dataSources, 11 | formatError, 12 | introspection, 13 | playground, 14 | resolvers, 15 | typeDefs, 16 | }); 17 | 18 | export default server; 19 | -------------------------------------------------------------------------------- /src/apollo/config/context.js: -------------------------------------------------------------------------------- 1 | async function createContext({ req, res }) { 2 | const context = { req, res }; 3 | 4 | return context; 5 | } 6 | 7 | export default createContext; 8 | -------------------------------------------------------------------------------- /src/apollo/config/formatError.js: -------------------------------------------------------------------------------- 1 | export default (error) => { 2 | console.log('[formatError]: ', error); 3 | console.log(error?.extensions?.exception?.stacktrace); 4 | 5 | return error; 6 | }; 7 | -------------------------------------------------------------------------------- /src/apollo/config/formatResponse.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | function formatResponse(props, { context: contextObj = {} }) {} 3 | 4 | export default formatResponse; 5 | -------------------------------------------------------------------------------- /src/apollo/config/index.js: -------------------------------------------------------------------------------- 1 | export context from './context'; 2 | export formatError from './formatError'; 3 | export playground from './playground'; 4 | export formatResponse from './formatResponse'; 5 | -------------------------------------------------------------------------------- /src/apollo/config/playground.js: -------------------------------------------------------------------------------- 1 | // https://www.apollographql.com/docs/apollo-server/features/graphql-playground.html 2 | 3 | import { enablePlayground, PORT } from '../../config'; 4 | 5 | const EXAMPLE_QUERY = `query getExamples { 6 | ping 7 | numberSix 8 | numberSeven 9 | user(id: 1) { 10 | id 11 | name 12 | } 13 | aliased_user: user(id: 2) { 14 | id 15 | name 16 | } 17 | randomDog { 18 | url 19 | } 20 | } 21 | `; 22 | 23 | const playgroundConfig = { 24 | settings: { 25 | 'editor.theme': 'dark', // accepts 'light' as well 26 | 'request.credentials': 'same-origin', // For sending browser cookies along with the GraphQL requests. use 'omit' to not send browser cookies 27 | }, 28 | tabs: [ 29 | { 30 | endpoint: `http://localhost:${PORT}/graphql`, 31 | query: EXAMPLE_QUERY, 32 | }, 33 | ], 34 | }; 35 | 36 | export default enablePlayground ? playgroundConfig : false; 37 | -------------------------------------------------------------------------------- /src/apollo/data-sources/ExampleAPI/ExampleAPI.js: -------------------------------------------------------------------------------- 1 | import { RESTDataSource } from 'apollo-datasource-rest'; 2 | import Boom from '@hapi/boom'; 3 | 4 | class ExampleAPI extends RESTDataSource { 5 | constructor() { 6 | super(); 7 | this.baseURL = 'https://random.dog/'; 8 | } 9 | 10 | willSendRequest(request) { 11 | if (!request.headers) request.headers = {}; 12 | 13 | request.headers['x-Custom-Req-Header-Example'] = ''; 14 | } 15 | 16 | async getRandomDog() { 17 | return this.get('/woof.json').catch((error) => { 18 | console.log('ERROR: ExampleAPI.getRandomDog - ', error.message); 19 | 20 | throw Boom.badImplementation(); 21 | }); 22 | } 23 | } 24 | 25 | export default ExampleAPI; 26 | -------------------------------------------------------------------------------- /src/apollo/data-sources/ExampleAPI/index.js: -------------------------------------------------------------------------------- 1 | export default from './ExampleAPI'; 2 | -------------------------------------------------------------------------------- /src/apollo/data-sources/index.js: -------------------------------------------------------------------------------- 1 | import ExampleAPI from './ExampleAPI'; 2 | 3 | export default () => ({ 4 | ExampleAPI: new ExampleAPI(), 5 | }); 6 | -------------------------------------------------------------------------------- /src/apollo/index.js: -------------------------------------------------------------------------------- 1 | export default from './apollo'; 2 | -------------------------------------------------------------------------------- /src/apollo/resolvers/ExampleResolver/ExampleResolver.js: -------------------------------------------------------------------------------- 1 | const users = [ 2 | { 3 | id: '1', 4 | name: 'Elizabeth Bennet', 5 | }, 6 | { 7 | id: '2', 8 | name: 'Fitzwilliam Darcy', 9 | }, 10 | ]; 11 | 12 | export default { 13 | Query: { 14 | numberSix() { 15 | return 6; 16 | }, 17 | numberSeven() { 18 | return 7; 19 | }, 20 | user(_, { id }) { 21 | return users.find((user) => user.id === id); 22 | }, 23 | async randomDog(_, __, { dataSources }) { 24 | const { ExampleAPI } = dataSources; 25 | 26 | const response = await ExampleAPI.getRandomDog(); 27 | 28 | return { 29 | url: response.url, 30 | }; 31 | }, 32 | }, 33 | Mutation: {}, 34 | }; 35 | -------------------------------------------------------------------------------- /src/apollo/resolvers/ExampleResolver/index.js: -------------------------------------------------------------------------------- 1 | export default from './ExampleResolver'; 2 | -------------------------------------------------------------------------------- /src/apollo/resolvers/index.js: -------------------------------------------------------------------------------- 1 | import ExampleResolver from './ExampleResolver'; 2 | 3 | const baseResolvers = { 4 | Query: { 5 | ping() { 6 | return 'pong'; 7 | }, 8 | }, 9 | }; 10 | 11 | export default [baseResolvers, ExampleResolver]; 12 | -------------------------------------------------------------------------------- /src/apollo/schema/schema.graphql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: Query 3 | mutation: Mutation 4 | } 5 | 6 | type Query { 7 | ping: String # Should always return a string with value "pong" 8 | numberSix: Int! # Should always return the number 6 when queried 9 | numberSeven: Int! # Should always return 7 10 | user(id: ID!): User 11 | randomDog: RandomDog 12 | } 13 | 14 | type Mutation { 15 | _: String # This is just a place holder - do not touch 16 | } 17 | 18 | type User { 19 | id: Int! 20 | name: String 21 | } 22 | 23 | type RandomDog { 24 | url: String 25 | } 26 | -------------------------------------------------------------------------------- /src/config/apollo.js: -------------------------------------------------------------------------------- 1 | // ############################################################################### 2 | // Apollo Server settings 3 | // 4 | 5 | export const isProd = process.env.NODE_ENV === 'production'; 6 | 7 | export const enablePlayground = 8 | process.env.NODE_ENV !== 'production' || process.env.APOLLO_ENABLE_PLAYGROUND === 'true'; 9 | 10 | export const introspection = 11 | process.env.NODE_ENV !== 'production' || process.env.APOLLO_ENABLE_INTROSPECTION === 'true'; 12 | -------------------------------------------------------------------------------- /src/config/index.js: -------------------------------------------------------------------------------- 1 | import { castIntEnv } from './lib'; 2 | 3 | export * from './apollo'; 4 | 5 | export const PORT = castIntEnv('PORT', 3001); 6 | -------------------------------------------------------------------------------- /src/config/lib/index.js: -------------------------------------------------------------------------------- 1 | export { castBooleanEnv, castIntEnv, castStringArrayEnv } from './utils'; 2 | -------------------------------------------------------------------------------- /src/config/lib/utils.js: -------------------------------------------------------------------------------- 1 | // ############################################################################### 2 | // Helpers for casting environment variables 3 | // 4 | 5 | export const castBooleanEnv = (envVar, defaultValue = false) => 6 | process.env[envVar] ? process.env[envVar]?.toLowerCase() === 'true' : defaultValue; 7 | 8 | export const castIntEnv = (envVar, defaultValue) => 9 | parseInt(process.env[envVar], 10) || defaultValue; 10 | 11 | export const castStringArrayEnv = (envVar) => 12 | process.env[envVar]?.length ? process.env[envVar].split(',').map((field) => field.trim()) : []; 13 | -------------------------------------------------------------------------------- /src/express/app.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import bodyParser from 'body-parser'; 3 | import cookieParser from 'cookie-parser'; 4 | import routes from './routes'; 5 | import { ignoreFavicon, logger } from './middleware'; 6 | 7 | const app = express(); // Express App with middleware initiation 8 | 9 | app.disable('x-powered-by'); // Disable "powered-by" header for security 10 | app.set('trust proxy', true); // allows reverse proxy trust 11 | 12 | app.use(bodyParser.json()); // Takes raw requests and turns them into usable properties on req.body 13 | app.use(bodyParser.urlencoded({ extended: true })); 14 | app.use(ignoreFavicon); // ignore favicon requests 15 | app.use(logger); // request logger 16 | app.use(cookieParser()); 17 | app.use('/api', routes); // routing 18 | 19 | export default app; 20 | -------------------------------------------------------------------------------- /src/express/controllers/API/API.js: -------------------------------------------------------------------------------- 1 | import { version } from '../../../../package.json'; 2 | import { PORT } from '../../../config'; 3 | 4 | const endpoint = `http://localhost:${PORT}`; 5 | class API { 6 | static getHealthStatus(req, res) { 7 | return res.status(200).send('OK'); 8 | } 9 | 10 | static getVersion(req, res) { 11 | return res.send(version); 12 | } 13 | 14 | static getWorld(req, res) { 15 | return res.json({ 16 | hello: 'world', 17 | }); 18 | } 19 | 20 | static overview(req, res) { 21 | return res.json({ 22 | api: { 23 | health: `${endpoint}/api/health`, 24 | hello_world: `${endpoint}/api/hello-world`, 25 | version: `${endpoint}/api/version`, 26 | }, 27 | graphql: `${endpoint}/graphql`, 28 | }); 29 | } 30 | } 31 | 32 | export default API; 33 | -------------------------------------------------------------------------------- /src/express/controllers/API/index.js: -------------------------------------------------------------------------------- 1 | export default from './API'; 2 | -------------------------------------------------------------------------------- /src/express/controllers/index.js: -------------------------------------------------------------------------------- 1 | export API from './API'; 2 | -------------------------------------------------------------------------------- /src/express/index.js: -------------------------------------------------------------------------------- 1 | export default from './app'; 2 | -------------------------------------------------------------------------------- /src/express/middleware/captureErrors.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-unused-vars 2 | function captureErrors(error, req, res, next) { 3 | const statusCode = error?.output?.statusCode || 500; 4 | const payload = error?.output?.payload || { 5 | statusCode, 6 | error: error.error || error.message, 7 | message: error.message, 8 | }; 9 | 10 | res.status(statusCode).send(payload); 11 | } 12 | 13 | export default captureErrors; 14 | -------------------------------------------------------------------------------- /src/express/middleware/errors.js: -------------------------------------------------------------------------------- 1 | // This is a custom error middleware for Express. 2 | // https://expressjs.com/en/guide/error-handling.html 3 | // eslint-disable-next-line no-unused-vars 4 | async function errors(err, _req, res, next) { 5 | const code = err?.output?.statusCode || 400; 6 | 7 | // log error 8 | console.error(err); // eslint-disable-line no-console 9 | 10 | // The default error message looks like this. 11 | const error = err?.output?.payload || { 12 | statusCode: code, 13 | error: code === 400 ? 'Bad Request' : 'Internal Server Error', 14 | message: err?.details?.[0]?.message, 15 | }; 16 | 17 | return res.status(code).send({ ...error }); 18 | } 19 | 20 | export default errors; 21 | -------------------------------------------------------------------------------- /src/express/middleware/ignoreFavicon.js: -------------------------------------------------------------------------------- 1 | function ignoreFavicon(req, res, next) { 2 | if (req.originalUrl.includes('favicon.ico')) { 3 | res.status(204).end(); 4 | return; 5 | } 6 | 7 | next(); 8 | } 9 | 10 | export default ignoreFavicon; 11 | -------------------------------------------------------------------------------- /src/express/middleware/index.js: -------------------------------------------------------------------------------- 1 | export captureErrors from './captureErrors'; 2 | export ignoreFavicon from './ignoreFavicon'; 3 | export logger from './logger'; 4 | export notFound from './notFound'; 5 | -------------------------------------------------------------------------------- /src/express/middleware/logger.js: -------------------------------------------------------------------------------- 1 | import morgan from 'morgan'; 2 | 3 | morgan.token('gql', (req) => req?.body?.operationName); 4 | 5 | const options = { 6 | skip(req) { 7 | const noBody = !Object.keys(req?.body || {}).length; 8 | const isIntrospectionQuery = req?.body?.operationName === 'IntrospectionQuery'; 9 | 10 | if (noBody || isIntrospectionQuery) return true; 11 | 12 | return false; 13 | }, 14 | }; 15 | 16 | const formats = { 17 | development: ':method :url :gql :status :response-time ms - :res[content-length]', 18 | production: ':method :url :gql :status :response-time ms - :res[content-length]', 19 | }; 20 | 21 | const format = formats[process.env.NODE_ENV === 'production' ? 'production' : 'development']; 22 | 23 | const logger = morgan(format, options); 24 | 25 | export default logger; 26 | -------------------------------------------------------------------------------- /src/express/middleware/notFound.js: -------------------------------------------------------------------------------- 1 | import Boom from '@hapi/boom'; 2 | 3 | function notFound() { 4 | throw Boom.notFound(); 5 | } 6 | 7 | export default notFound; 8 | -------------------------------------------------------------------------------- /src/express/routes/api.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { API } from '../controllers'; 3 | 4 | const router = Router(); 5 | 6 | router.get('/health', API.getHealthStatus); 7 | router.get('/version', API.getVersion); 8 | router.get('/hello-world', API.getWorld); 9 | router.get('/', API.overview); 10 | 11 | export default router; 12 | -------------------------------------------------------------------------------- /src/express/routes/index.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import apiRouter from './api'; 3 | import { notFound, captureErrors } from '../middleware'; 4 | 5 | const router = Router(); 6 | 7 | router.use('/', apiRouter); 8 | router.use('*', notFound, captureErrors); 9 | 10 | export default router; 11 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { PORT } from './config'; 2 | import apollo from './apollo'; 3 | import app from './express'; 4 | 5 | async function start() { 6 | apollo.applyMiddleware({ app }); // apply app middleware to apollo server 7 | 8 | // listen on port and log start up url 9 | app.listen({ port: PORT }, () => 10 | console.log( 11 | `restAPI: http://localhost:${PORT}/api\nGraphQL: http://localhost:${ 12 | PORT + apollo.graphqlPath 13 | }\n`, 14 | ), 15 | ); 16 | } 17 | 18 | if (process.env.NODE_ENV !== 'test') start(); 19 | --------------------------------------------------------------------------------