diff --git a/.eslintrc b/.eslintrc
index 6a6f8e8..08a119a 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -4,6 +4,9 @@
"airbnb",
"plugin:jsx-a11y/recommended"
],
+ "globals": {
+ "Kit": true
+ },
"env": {
"node": true,
"browser": true,
diff --git a/README.md b/README.md
index f5a8355..9759555 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,14 @@
This starter kit is designed to help you start project as soon as possible. It contain all necessary things to develop and maintain a project.
+## Before start
+
+You need to read these docs:
+
+1. [Error handling](/docs/error.md)
+
+Remove all code which we use for showing template opportunities. We left indicator for every place which need to remove `// REMOVE_PROD`
+
## Getting started
In this project we used [ESLint](https://eslint.org/docs/user-guide/integrations#editors) and [Stylelint](https://stylelint.io/user-guide/complementary-tools/#editor-plugins), so at the beginning check your code editor plugins or settings to support these linters.
diff --git a/client/components/common/Navigation.jsx b/client/components/common/Navigation.jsx
index 6019119..0dc6e5a 100644
--- a/client/components/common/Navigation.jsx
+++ b/client/components/common/Navigation.jsx
@@ -15,6 +15,9 @@ const Navigation = () => (
Form components
+
+ Example page
+
diff --git a/client/pages/Example.jsx b/client/pages/Example.jsx
new file mode 100644
index 0000000..d96e9d6
--- /dev/null
+++ b/client/pages/Example.jsx
@@ -0,0 +1,30 @@
+import React, { Component } from 'react';
+
+import { Button } from 'Form';
+
+class ExampleComponents extends Component {
+ state = {
+ error: '',
+ }
+
+ showError = () => {
+ fetch('/api/test/error')
+ .then(response => response.json())
+ .then(data => this.setState({ error: JSON.stringify(data) }));
+ }
+
+ render() {
+ const { error } = this.state;
+
+ return (
+
+
Show Error Message
+
+ {`Error: ${error}`}
+
+
+ );
+ }
+}
+
+export default ExampleComponents;
diff --git a/client/routing/Routes.jsx b/client/routing/Routes.jsx
index 06e34ba..85a6dfc 100644
--- a/client/routing/Routes.jsx
+++ b/client/routing/Routes.jsx
@@ -5,6 +5,7 @@ import MainLayout from 'Layout/Main';
import Index from 'Pages/Index';
import FormComponents from 'Pages/FormComponents';
+import ExampleComponents from 'Pages/Example';
import NoMatch from 'Pages/NoMatch';
const Routes = () => (
@@ -13,6 +14,7 @@ const Routes = () => (
+
diff --git a/docs/error.md b/docs/error.md
new file mode 100644
index 0000000..f1ccc55
--- /dev/null
+++ b/docs/error.md
@@ -0,0 +1,127 @@
+# Error handling
+
+This is a document how we most work with **Error** on current project.
+
+For correct works with **Error** in project please read [this article](https://expressjs.com/en/guide/error-handling.html) and use `Kit.CustomError` as `Error` object
+
+Example of handling:
+
+```js
+router.get('/error', (req, res, next) => {
+ try {
+ // ...some code
+ } catch(e) {
+ next(new Kit.CustomError('UNAUTHORIZED_ACCESS', 401));
+ }
+});
+```
+
+## Error structure
+
+This structure you must send to client when error is happened and you can get this structure from `Kit.CustomError` by `.get()` method
+
+```js
+{
+ "errors": [
+ {
+ "parameter": "start_time",
+ "details": "invalid date",
+ "code": "INVALID_PARAMETER",
+ "value": "",
+ "message": "Expected time, got \"\" for start_time",
+ "userMessage": "Expected time, got \"\" for start_time"
+ }
+ ],
+ "request": {
+ "params": {
+ "account_id": "hkk5"
+ }
+ },
+ "metadata": {}
+}
+```
+
+## Error codes & what they mean
+
+
+
+
+ HTTP Code
+ Error Code
+
+
+
+
+ 403
+ ACCOUNT_NOT_FOUND
+
+
+ 403
+ ACTION_NOT_ALLOWED
+
+
+ 400
+ EXCLUSIVE_PARAMETERS
+
+
+ 400
+ FEATURE_NOT_AVAILABLE
+
+
+ 400
+ ILLEGAL_CHARACTERS
+
+
+ 500
+ INTERNAL_ERROR
+
+
+ 400
+ INVALID_PARAMETER
+
+
+ 400
+ INVALID_USER
+
+
+ 400
+ INVALID_USER_ID
+
+
+ 400
+ MISSING_PARAMETERS
+
+
+ 404
+ NOT_FOUND
+
+
+ 400
+ REQUEST_TOO_COMPLEX
+
+
+ 404
+ ROUTE_NOT_FOUND
+
+
+ 503
+ SERVICE_UNAVAILABLE
+
+
+ 503
+ OVER_CAPACITY
+
+
+ 429
+ TOO_MANY_REQUESTS
+
+
+ 401
+ UNAUTHORIZED_ACCESS
+
+
+ 403
+ USER_NOT_FOUND
+
+
+
diff --git a/server/config/error.js b/server/config/error.js
new file mode 100644
index 0000000..0f40a3e
--- /dev/null
+++ b/server/config/error.js
@@ -0,0 +1,97 @@
+module.exports = {
+ ACCOUNT_NOT_FOUND: {
+ message: 'Account not found',
+ userMessage: 'Account not found',
+ code: 403,
+ },
+ ACTION_NOT_ALLOWED: {
+ message: 'Action not allowed',
+ userMessage: 'Action not allowed',
+ code: 403,
+ },
+ EXCLUSIVE_PARAMETERS: {
+ message: 'Exclusive parameters',
+ userMessage: 'Exclusive parameters',
+ code: 400,
+ },
+ FEATURE_NOT_AVAILABLE: {
+ message: 'Feature not available',
+ userMessage: 'Feature not available',
+ code: 400,
+ },
+ ILLEGAL_CHARACTERS: {
+ message: 'Illegal characters',
+ userMessage: 'Illegal characters',
+ code: 400,
+ },
+ INTERNAL_ERROR: {
+ message: 'Server error',
+ userMessage: 'Server error',
+ code: 500,
+ },
+ INVALID_PARAMETER: {
+ message: 'Invalid parameter',
+ userMessage: 'Invalid parameter',
+ code: 400,
+ },
+ INVALID_USER: {
+ message: 'Invalid user',
+ userMessage: 'Invalid user',
+ code: 400,
+ },
+ INVALID_USER_ID: {
+ message: 'Invalid user ID',
+ userMessage: 'Invalid user ID',
+ code: 400,
+ },
+ MISSING_PARAMETERS: {
+ message: 'Missing parameters',
+ userMessage: 'Missing parameters',
+ code: 400,
+ },
+ NOT_FOUND: {
+ message: 'Not Found',
+ userMessage: 'Not Found',
+ code: 404,
+ },
+ REQUEST_TOO_COMPLEX: {
+ message: 'Request too complex',
+ userMessage: 'Request too complex',
+ code: 400,
+ },
+ ROUTE_NOT_FOUND: {
+ message: 'Route not found',
+ userMessage: 'Route not found',
+ code: 404,
+ },
+ SERVICE_UNAVAILABLE: {
+ message: 'Service unavailable',
+ userMessage: 'Service unavailable',
+ code: 503,
+ },
+ OVER_CAPACITY: {
+ message: 'Over capacity',
+ userMessage: 'Over capacity',
+ code: 503,
+ },
+ TOO_MANY_REQUESTS: {
+ message: 'Too many request',
+ userMessage: 'Too many request',
+ code: 429,
+ },
+ UNAUTHORIZED_ACCESS: {
+ message: 'Unathorized access',
+ userMessage: 'Unathorized access',
+ code: 401,
+ },
+ USER_NOT_FOUND: {
+ message: 'User not found',
+ userMessage: 'User not found',
+ code: 403,
+ },
+ FORBIDDEN: {
+ message: 'Forbidden',
+ userMessage: 'Forbidden',
+ code: 403,
+ },
+};
diff --git a/server/index.js b/server/index.js
index b977ab4..c63a7c6 100644
--- a/server/index.js
+++ b/server/index.js
@@ -9,9 +9,14 @@ const methodOverride = require('method-override');
const swaggerUIDist = require('swagger-ui-dist');
const routes = require('./routes/index.route');
+const CustomError = require('./utils/error');
+const errorHandler = require('./middlewares/error');
require('dotenv-safe').config();
+global.Kit = {};
+Kit.CustomError = CustomError;
+
const app = express();
app.db = require('./models');
@@ -35,6 +40,8 @@ if (!isProduction) {
app.get('/*', (req, res) => res.sendFile(path.join(__dirname, `../${isProduction ? 'dist' : 'client'}/index.html`)));
+app.use(errorHandler);
+
const port = process.env.APP_PORT || 3001;
const host = process.env.APP_HOST || 'localhost';
diff --git a/server/middlewares/error.js b/server/middlewares/error.js
new file mode 100644
index 0000000..0ec1f0b
--- /dev/null
+++ b/server/middlewares/error.js
@@ -0,0 +1,19 @@
+function errorHandling(err, req, res, next) {
+ if (res.headersSent) {
+ return next(err);
+ }
+
+ res.status(err.code);
+ return res.json(err.get());
+}
+
+// process.on('unhandledRejection', (err) => {
+// console.error(err);
+// });
+
+// process.on('uncaughtException', (err) => {
+// console.error(err);
+// process.exit(1);
+// });
+
+module.exports = errorHandling;
diff --git a/server/routes/index.route.js b/server/routes/index.route.js
index ff85702..3456095 100644
--- a/server/routes/index.route.js
+++ b/server/routes/index.route.js
@@ -2,6 +2,8 @@ const express = require('express');
const authRoutes = require('./auth.route');
const userRoutes = require('./user.route');
+// REMOVE_PROD: in real app you need remove this variable
+const testRoutes = require('./test.route');
const router = express.Router();
@@ -11,4 +13,8 @@ router.use('/auth', authRoutes);
// mount user routes at /user
router.use('/user', userRoutes);
+// REMOVE_PROD: in real app you need remove this route
+router.use('/test', testRoutes);
+
+
module.exports = router;
diff --git a/server/routes/test.route.js b/server/routes/test.route.js
new file mode 100644
index 0000000..f07b80f
--- /dev/null
+++ b/server/routes/test.route.js
@@ -0,0 +1,9 @@
+const express = require('express');
+
+const router = express.Router();
+
+router.get('/error', (req, res, next) => {
+ next(new Kit.CustomError('UNAUTHORIZED_ACCESS', 401));
+});
+
+module.exports = router;
diff --git a/server/tests/error.test.js b/server/tests/error.test.js
new file mode 100644
index 0000000..a42ee49
--- /dev/null
+++ b/server/tests/error.test.js
@@ -0,0 +1,35 @@
+// REMOVE_PROD: remove this file
+
+const _ = require('lodash');
+const base = require('./base.test');
+const Errors = require('../config/error');
+
+const { request, truncate } = base;
+const apiBase = process.env.API_BASE || '/api';
+
+describe('GET /test/error', () => {
+ before(() => truncate());
+ after(() => truncate());
+
+ it('error structure', () => request
+ .get(`${apiBase}/test/error`)
+ .expect('Content-Type', /json/)
+ .expect(401)
+ .expect(({ body }) => {
+ if (!(Array.isArray(body.errors) && body.errors.length && body.request && body.metadata)) {
+ throw new Error(`
+ Error response must have next structure:
+ {
+ "errors": [{ ... }],
+ "request": { ... },
+ "metadata": {}
+ }
+ `);
+ }
+ const error = _.get(body, 'errors[0]', {});
+
+ if (error.userMessage !== Errors.UNAUTHORIZED_ACCESS.userMessage && error.code !== 401) {
+ throw new Error('Error has incorrect data');
+ }
+ }));
+});
diff --git a/server/utils/error.js b/server/utils/error.js
new file mode 100644
index 0000000..1c99736
--- /dev/null
+++ b/server/utils/error.js
@@ -0,0 +1,68 @@
+const _ = require('lodash');
+const ErrorHash = require('../config/error');
+
+const isProduction = process.env.NODE_ENV === 'production';
+
+const defaultError = {
+ parameter: '',
+ details: '',
+ code: 'ERROR',
+ value: '',
+ message: 'Something went wrong.',
+ userMessage: 'Something went wrong.',
+};
+
+class CustomError extends Error {
+ static createError(errorCode) {
+ const error = ErrorHash[errorCode];
+
+ if (!error) {
+ return {
+ ...defaultError,
+ code: errorCode,
+ };
+ }
+
+ return {
+ ...defaultError,
+ ...error,
+ };
+ }
+
+ constructor(errorCode, httpCode, ...params) {
+ super(params);
+ this.errors = [CustomError.createError(errorCode)];
+ this.code = httpCode || this.errors[0].code;
+ this.message = this.errors[0].message;
+ this.name = 'Server error';
+ this.request = {};
+ this.metadata = {};
+ }
+
+ get() {
+ const errors = this.errors.slice().map((error) => {
+ if (isProduction) {
+ // eslint-disable-next-line
+ delete error.message;
+ }
+
+ return error;
+ });
+
+ return {
+ errors,
+ request: this.request,
+ metadata: this.metadata,
+ };
+ }
+
+ upsertMetadata(data) {
+ if (!_.isObject(data)) {
+ throw new Error("Metadata must be 'object'");
+ }
+
+ this.metadata = this.metadata ? { ...this.metadata, ...data } : data;
+ }
+}
+
+module.exports = CustomError;