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/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 (
+
+
+
+ {`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..a18cd68
--- /dev/null
+++ b/docs/error.md
@@ -0,0 +1,128 @@
+# Error handling
+
+This is a document how we most work with error on current project.
+
+## Error handling
+
+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"
+ }
+ ],
+ "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..0e2ac9a
--- /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/utils/error.js b/server/utils/error.js
new file mode 100644
index 0000000..0a409b7
--- /dev/null
+++ b/server/utils/error.js
@@ -0,0 +1,67 @@
+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 = null;
+ }
+
+ get() {
+ const errors = this.errors.slice().map((error) => {
+ if (isProduction) {
+ 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;