From 5435ce93ee6d6c3f0618a2d37f18b6c016f81495 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tadeusz=20Miesi=C4=85c?= <tadeusz.miesiac@gmail.com>
Date: Wed, 27 Sep 2023 16:04:50 +0200
Subject: [PATCH] feat(async redux example): added template for async
 communication with redux thunk

---
 next-env.d.ts                           |   1 +
 package-lock.json                       | 180 ++++++++++++++++++++++--
 package.json                            |   4 +-
 pages/{redux.tsx => redux-api-poc.tsx}  |   7 +-
 src/constants/.gitkeep                  |   0
 src/constants/api.ts                    |   1 +
 src/models/disease.ts                   |   9 ++
 src/models/organism.ts                  |   9 ++
 src/models/project.ts                   |  47 +++++++
 src/redux/project/project.reducers.ts   |  10 ++
 src/redux/project/project.slice.ts      |  20 +++
 src/redux/project/project.thunks.ts     |  16 +++
 src/redux/project/project.types.ts      |   7 +
 src/redux/store.ts                      |   4 +-
 src/services/.gitkeep                   |   0
 src/services/api/utils/axiosInstance.ts |   6 +
 src/services/api/utils/useApiQuery.ts   |  39 +++++
 src/types/.gitkeep                      |   0
 src/types/api.ts                        |  18 +++
 src/utils/.gitkeep                      |   0
 src/utils/validateDataUsingZodSchema.ts |  15 ++
 21 files changed, 378 insertions(+), 15 deletions(-)
 rename pages/{redux.tsx => redux-api-poc.tsx} (69%)
 delete mode 100644 src/constants/.gitkeep
 create mode 100644 src/constants/api.ts
 create mode 100644 src/models/disease.ts
 create mode 100644 src/models/organism.ts
 create mode 100644 src/models/project.ts
 create mode 100644 src/redux/project/project.reducers.ts
 create mode 100644 src/redux/project/project.slice.ts
 create mode 100644 src/redux/project/project.thunks.ts
 create mode 100644 src/redux/project/project.types.ts
 delete mode 100644 src/services/.gitkeep
 create mode 100644 src/services/api/utils/axiosInstance.ts
 create mode 100644 src/services/api/utils/useApiQuery.ts
 delete mode 100644 src/types/.gitkeep
 create mode 100644 src/types/api.ts
 delete mode 100644 src/utils/.gitkeep
 create mode 100644 src/utils/validateDataUsingZodSchema.ts

diff --git a/next-env.d.ts b/next-env.d.ts
index 53e1f337..fd36f949 100644
--- a/next-env.d.ts
+++ b/next-env.d.ts
@@ -1,4 +1,5 @@
 /// <reference types="next" />
+/// <reference types="next/image-types/global" />
 /// <reference types="next/navigation-types/compat/navigation" />
 
 // NOTE: This file should not be edited
diff --git a/package-lock.json b/package-lock.json
index 02f72268..1a168c99 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,6 +15,7 @@
         "@types/react": "18.2.21",
         "@types/react-dom": "18.2.7",
         "autoprefixer": "10.4.15",
+        "axios": "^1.5.1",
         "eslint-config-next": "13.4.19",
         "next": "13.4.19",
         "postcss": "8.4.29",
@@ -22,7 +23,8 @@
         "react-dom": "18.2.0",
         "react-redux": "^8.1.2",
         "tailwind-merge": "^1.14.0",
-        "tailwindcss": "3.3.3"
+        "tailwindcss": "3.3.3",
+        "zod": "^3.22.2"
       },
       "devDependencies": {
         "@commitlint/cli": "^17.7.1",
@@ -1966,6 +1968,21 @@
       "resolved": "https://registry.npmjs.org/@next/font/-/font-13.5.2.tgz",
       "integrity": "sha512-c9EXqdXMEErMLrC71wZvpcOnNVkEEufZOO3EjgQJcKQUwPISvnkgIj9GKFIop0rX2dLNdzL3OC/4nrcAqWqUsg=="
     },
+    "node_modules/@next/swc-darwin-arm64": {
+      "version": "13.4.19",
+      "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.19.tgz",
+      "integrity": "sha512-vv1qrjXeGbuF2mOkhkdxMDtv9np7W4mcBtaDnHU+yJG+bBwa6rYsYSCI/9Xm5+TuF5SbZbrWO6G1NfTh1TMjvQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
     "node_modules/@next/swc-darwin-x64": {
       "version": "13.4.19",
       "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.19.tgz",
@@ -1981,6 +1998,111 @@
         "node": ">= 10"
       }
     },
+    "node_modules/@next/swc-linux-arm64-gnu": {
+      "version": "13.4.19",
+      "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.19.tgz",
+      "integrity": "sha512-vdlnIlaAEh6H+G6HrKZB9c2zJKnpPVKnA6LBwjwT2BTjxI7e0Hx30+FoWCgi50e+YO49p6oPOtesP9mXDRiiUg==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-linux-arm64-musl": {
+      "version": "13.4.19",
+      "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.19.tgz",
+      "integrity": "sha512-aU0HkH2XPgxqrbNRBFb3si9Ahu/CpaR5RPmN2s9GiM9qJCiBBlZtRTiEca+DC+xRPyCThTtWYgxjWHgU7ZkyvA==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-linux-x64-gnu": {
+      "version": "13.4.19",
+      "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.19.tgz",
+      "integrity": "sha512-htwOEagMa/CXNykFFeAHHvMJeqZfNQEoQvHfsA4wgg5QqGNqD5soeCer4oGlCol6NGUxknrQO6VEustcv+Md+g==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-linux-x64-musl": {
+      "version": "13.4.19",
+      "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.19.tgz",
+      "integrity": "sha512-4Gj4vvtbK1JH8ApWTT214b3GwUh9EKKQjY41hH/t+u55Knxi/0wesMzwQRhppK6Ddalhu0TEttbiJ+wRcoEj5Q==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-win32-arm64-msvc": {
+      "version": "13.4.19",
+      "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.19.tgz",
+      "integrity": "sha512-bUfDevQK4NsIAHXs3/JNgnvEY+LRyneDN788W2NYiRIIzmILjba7LaQTfihuFawZDhRtkYCv3JDC3B4TwnmRJw==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-win32-ia32-msvc": {
+      "version": "13.4.19",
+      "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.19.tgz",
+      "integrity": "sha512-Y5kikILFAr81LYIFaw6j/NrOtmiM4Sf3GtOc0pn50ez2GCkr+oejYuKGcwAwq3jiTKuzF6OF4iT2INPoxRycEA==",
+      "cpu": [
+        "ia32"
+      ],
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-win32-x64-msvc": {
+      "version": "13.4.19",
+      "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.19.tgz",
+      "integrity": "sha512-YzA78jBDXMYiINdPdJJwGgPNT3YqBNNGhsthsDoWHL9p24tEJn9ViQf/ZqTbwSpX/RrkPupLfuuTH2sf73JBAw==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
     "node_modules/@nodelib/fs.scandir": {
       "version": "2.1.5",
       "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -3043,8 +3165,7 @@
     "node_modules/asynckit": {
       "version": "0.4.0",
       "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
-      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
-      "dev": true
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
     },
     "node_modules/at-least-node": {
       "version": "1.0.0",
@@ -3125,6 +3246,21 @@
         "node": ">=4"
       }
     },
+    "node_modules/axios": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz",
+      "integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==",
+      "dependencies": {
+        "follow-redirects": "^1.15.0",
+        "form-data": "^4.0.0",
+        "proxy-from-env": "^1.1.0"
+      }
+    },
+    "node_modules/axios/node_modules/proxy-from-env": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+      "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
+    },
     "node_modules/axobject-query": {
       "version": "3.2.1",
       "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz",
@@ -3839,7 +3975,6 @@
       "version": "1.0.8",
       "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
       "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
-      "dev": true,
       "dependencies": {
         "delayed-stream": "~1.0.0"
       },
@@ -4845,7 +4980,6 @@
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
       "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
-      "dev": true,
       "engines": {
         "node": ">=0.4.0"
       }
@@ -6262,6 +6396,25 @@
       "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz",
       "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ=="
     },
+    "node_modules/follow-redirects": {
+      "version": "1.15.3",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz",
+      "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/RubenVerborgh"
+        }
+      ],
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependenciesMeta": {
+        "debug": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/for-each": {
       "version": "0.3.3",
       "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
@@ -6283,7 +6436,6 @@
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
       "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
-      "dev": true,
       "dependencies": {
         "asynckit": "^0.4.0",
         "combined-stream": "^1.0.8",
@@ -9391,7 +9543,6 @@
       "version": "1.52.0",
       "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
       "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
-      "dev": true,
       "engines": {
         "node": ">= 0.6"
       }
@@ -9400,7 +9551,6 @@
       "version": "2.1.35",
       "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
       "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
-      "dev": true,
       "dependencies": {
         "mime-db": "1.52.0"
       },
@@ -9586,6 +9736,14 @@
         "node": "^10 || ^12 || >=14"
       }
     },
+    "node_modules/next/node_modules/zod": {
+      "version": "3.21.4",
+      "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz",
+      "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==",
+      "funding": {
+        "url": "https://github.com/sponsors/colinhacks"
+      }
+    },
     "node_modules/node-int64": {
       "version": "0.4.0",
       "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
@@ -12641,9 +12799,9 @@
       }
     },
     "node_modules/zod": {
-      "version": "3.21.4",
-      "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz",
-      "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==",
+      "version": "3.22.2",
+      "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.2.tgz",
+      "integrity": "sha512-wvWkphh5WQsJbVk1tbx1l1Ly4yg+XecD+Mq280uBGt9wa5BKSWf4Mhp6GmrkPixhMxmabYY7RbzlwVP32pbGCg==",
       "funding": {
         "url": "https://github.com/sponsors/colinhacks"
       }
diff --git a/package.json b/package.json
index 04cdab67..30e182e0 100644
--- a/package.json
+++ b/package.json
@@ -33,6 +33,7 @@
     "@types/react": "18.2.21",
     "@types/react-dom": "18.2.7",
     "autoprefixer": "10.4.15",
+    "axios": "^1.5.1",
     "eslint-config-next": "13.4.19",
     "next": "13.4.19",
     "postcss": "8.4.29",
@@ -40,7 +41,8 @@
     "react-dom": "18.2.0",
     "react-redux": "^8.1.2",
     "tailwind-merge": "^1.14.0",
-    "tailwindcss": "3.3.3"
+    "tailwindcss": "3.3.3",
+    "zod": "^3.22.2"
   },
   "devDependencies": {
     "@commitlint/cli": "^17.7.1",
diff --git a/pages/redux.tsx b/pages/redux-api-poc.tsx
similarity index 69%
rename from pages/redux.tsx
rename to pages/redux-api-poc.tsx
index e8bd8714..4a195e6e 100644
--- a/pages/redux.tsx
+++ b/pages/redux-api-poc.tsx
@@ -1,15 +1,18 @@
-import { useDispatch, useSelector } from 'react-redux';
+import { useSelector } from 'react-redux';
 import { selectSearchValue } from '@/redux/search/search.selectors';
 import { setSearchValue } from '@/redux/search/search.slice';
+import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
+import { getProjectById } from '@/redux/project/project.thunks';
 
 const ReduxPage = (): JSX.Element => {
-  const dispatch = useDispatch();
+  const dispatch = useAppDispatch();
   const searchValue = useSelector(selectSearchValue);
 
   const triggerSyncUpdate = (): void => {
     // eslint-disable-next-line prefer-template
     const newValue = searchValue + 'test';
     dispatch(setSearchValue(newValue));
+    dispatch(getProjectById('pd_map_winter_23'));
   };
 
   return (
diff --git a/src/constants/.gitkeep b/src/constants/.gitkeep
deleted file mode 100644
index e69de29b..00000000
diff --git a/src/constants/api.ts b/src/constants/api.ts
new file mode 100644
index 00000000..f3ebe0ec
--- /dev/null
+++ b/src/constants/api.ts
@@ -0,0 +1 @@
+export const BASE_API_URL = 'https://corsproxy.io/?https://pdmap.uni.lu/minerva/api';
diff --git a/src/models/disease.ts b/src/models/disease.ts
new file mode 100644
index 00000000..65079e66
--- /dev/null
+++ b/src/models/disease.ts
@@ -0,0 +1,9 @@
+import { z } from 'zod';
+
+export const disease = z.object({
+  link: z.string(),
+  type: z.string(),
+  resource: z.string(),
+  id: z.number(),
+  annotatorClassName: z.string(),
+});
diff --git a/src/models/organism.ts b/src/models/organism.ts
new file mode 100644
index 00000000..4b003eef
--- /dev/null
+++ b/src/models/organism.ts
@@ -0,0 +1,9 @@
+import { z } from 'zod';
+
+export const organism = z.object({
+  link: z.string(),
+  type: z.string(),
+  resource: z.string(),
+  id: z.number(),
+  annotatorClassName: z.string(),
+});
diff --git a/src/models/project.ts b/src/models/project.ts
new file mode 100644
index 00000000..051e5ca1
--- /dev/null
+++ b/src/models/project.ts
@@ -0,0 +1,47 @@
+import { z } from 'zod';
+import { disease } from './disease';
+import { organism } from './organism';
+
+export const projectSchema = z.object({
+  version: z.string(),
+  disease,
+  organism,
+  idObject: z.number(),
+  status: z.string(),
+  directory: z.string(),
+  progress: z.number(),
+  notifyEmail: z.string(),
+  logEntries: z.boolean(),
+  name: z.string(),
+  sharedInMinervaNet: z.boolean(),
+  owner: z.string(),
+  projectId: z.string(),
+  creationDate: z.string(),
+  mapCanvasType: z.string(),
+  overviewImageViews: z.array(
+    z.object({
+      idObject: z.number(),
+      filename: z.string(),
+      width: z.number(),
+      height: z.number(),
+      links: z.array(
+        z.union([
+          z.object({
+            idObject: z.number(),
+            polygon: z.array(z.object({ x: z.number(), y: z.number() })),
+            imageLinkId: z.number(),
+            type: z.string(),
+          }),
+          z.object({
+            idObject: z.number(),
+            polygon: z.array(z.object({ x: z.number(), y: z.number() })),
+            zoomLevel: z.number(),
+            modelPoint: z.object({ x: z.number(), y: z.number() }),
+            modelLinkId: z.number(),
+            type: z.string(),
+          }),
+        ]),
+      ),
+    }),
+  ),
+});
diff --git a/src/redux/project/project.reducers.ts b/src/redux/project/project.reducers.ts
new file mode 100644
index 00000000..aee885ae
--- /dev/null
+++ b/src/redux/project/project.reducers.ts
@@ -0,0 +1,10 @@
+import { ActionReducerMapBuilder } from '@reduxjs/toolkit';
+import { ProjectState } from '@/redux/project/project.types';
+import { getProjectById } from '@/redux/project/project.thunks';
+
+export const getProjectByIdReducer = (builder: ActionReducerMapBuilder<ProjectState>): void => {
+  builder.addCase(getProjectById.fulfilled, (state, action) => {
+    state.data = action.payload;
+    state.loading = 'succeeded';
+  });
+};
diff --git a/src/redux/project/project.slice.ts b/src/redux/project/project.slice.ts
new file mode 100644
index 00000000..994cb484
--- /dev/null
+++ b/src/redux/project/project.slice.ts
@@ -0,0 +1,20 @@
+import { createSlice } from '@reduxjs/toolkit';
+import { ProjectState } from '@/redux/project/project.types';
+import { getProjectByIdReducer } from './project.reducers';
+
+const initialState: ProjectState = {
+  data: [],
+  loading: 'idle',
+  error: { name: '', message: '' },
+};
+
+const projectSlice = createSlice({
+  name: 'project',
+  initialState,
+  reducers: {},
+  extraReducers: builder => {
+    getProjectByIdReducer(builder);
+  },
+});
+
+export default projectSlice.reducer;
diff --git a/src/redux/project/project.thunks.ts b/src/redux/project/project.thunks.ts
new file mode 100644
index 00000000..d26bbdda
--- /dev/null
+++ b/src/redux/project/project.thunks.ts
@@ -0,0 +1,16 @@
+import { createAsyncThunk } from '@reduxjs/toolkit';
+import { axiosInstance } from '@/services/api/utils/axiosInstance';
+import { Project } from '@/types/api';
+import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema';
+import { projectSchema } from '@/models/project';
+
+export const getProjectById = createAsyncThunk(
+  'project/getUsersByIdStatus',
+  async (id: string): Promise<Project | undefined> => {
+    const response = await axiosInstance.get<Project>(`projects/${id}`);
+
+    const isDataValid = validateDataUsingZodSchema(response.data, projectSchema);
+
+    return isDataValid ? response.data : undefined;
+  },
+);
diff --git a/src/redux/project/project.types.ts b/src/redux/project/project.types.ts
new file mode 100644
index 00000000..b5ace9d7
--- /dev/null
+++ b/src/redux/project/project.types.ts
@@ -0,0 +1,7 @@
+import { Project } from '@/types/api';
+
+export type ProjectState = {
+  data: Project | undefined | [];
+  loading: 'idle' | 'pending' | 'succeeded' | 'failed';
+  error: Error;
+};
diff --git a/src/redux/store.ts b/src/redux/store.ts
index 5d76dbde..d1335018 100644
--- a/src/redux/store.ts
+++ b/src/redux/store.ts
@@ -1,9 +1,11 @@
 import { configureStore } from '@reduxjs/toolkit';
-import searchReducer from './search/search.slice';
+import searchReducer from '@/redux/search/search.slice';
+import projectSlice from '@/redux/project/project.slice';
 
 export const store = configureStore({
   reducer: {
     search: searchReducer,
+    project: projectSlice,
   },
   devTools: true,
 });
diff --git a/src/services/.gitkeep b/src/services/.gitkeep
deleted file mode 100644
index e69de29b..00000000
diff --git a/src/services/api/utils/axiosInstance.ts b/src/services/api/utils/axiosInstance.ts
new file mode 100644
index 00000000..5e50fba7
--- /dev/null
+++ b/src/services/api/utils/axiosInstance.ts
@@ -0,0 +1,6 @@
+import axios from 'axios';
+import { BASE_API_URL } from '@/constants/api';
+
+export const axiosInstance = axios.create({
+  baseURL: BASE_API_URL,
+});
diff --git a/src/services/api/utils/useApiQuery.ts b/src/services/api/utils/useApiQuery.ts
new file mode 100644
index 00000000..d150d656
--- /dev/null
+++ b/src/services/api/utils/useApiQuery.ts
@@ -0,0 +1,39 @@
+import { BASE_API_URL } from '@/constants/api';
+import { QueryOptions } from '@/types/api';
+
+import useAxios, { UseAxiosResult } from 'axios-hooks';
+import { useMemo } from 'react';
+import { AnyZodObject, ZodError } from 'zod';
+
+type UseApiQuery = <TResponse extends AnyZodObject>(
+  queryOptions: QueryOptions<TResponse>,
+) => UseAxiosResult<TResponse>;
+
+export const useApiQuery: UseApiQuery = <TResponse extends AnyZodObject>({
+  method,
+  path,
+  response,
+}: QueryOptions<TResponse>) => {
+  const [{ data: fetchData, loading, error }, refetch, cancelRequest] = useAxios<TResponse>({
+    method,
+    url: `${BASE_API_URL}${path}`,
+  });
+
+  const dataValidation: { success: boolean; error?: ZodError<TResponse> } = useMemo(() => {
+    if (!fetchData) {
+      return { success: false, error: undefined };
+    }
+
+    return {
+      error: undefined,
+      ...response.safeParse(fetchData),
+    };
+  }, [fetchData, response]);
+
+  const data = useMemo(
+    () => (dataValidation.success ? fetchData : undefined),
+    [dataValidation.success, fetchData],
+  );
+
+  return [{ data, loading, error }, refetch, cancelRequest];
+};
diff --git a/src/types/.gitkeep b/src/types/.gitkeep
deleted file mode 100644
index e69de29b..00000000
diff --git a/src/types/api.ts b/src/types/api.ts
new file mode 100644
index 00000000..98a13076
--- /dev/null
+++ b/src/types/api.ts
@@ -0,0 +1,18 @@
+import { z } from 'zod';
+import { disease } from '@/models/disease';
+import { organism } from '@/models/organism';
+import { projectSchema } from '@/models/project';
+
+export interface QueryOptions<Response> {
+  method: 'GET' | 'POST';
+  path: string;
+  response: Response;
+}
+
+export interface Query<Params, Response> {
+  (params: Params): QueryOptions<Response>;
+}
+
+export type Project = z.infer<typeof projectSchema>;
+export type Organism = z.infer<typeof organism>;
+export type Disease = z.infer<typeof disease>;
diff --git a/src/utils/.gitkeep b/src/utils/.gitkeep
deleted file mode 100644
index e69de29b..00000000
diff --git a/src/utils/validateDataUsingZodSchema.ts b/src/utils/validateDataUsingZodSchema.ts
new file mode 100644
index 00000000..a7b0cf08
--- /dev/null
+++ b/src/utils/validateDataUsingZodSchema.ts
@@ -0,0 +1,15 @@
+import { ZodSchema } from 'zod';
+
+type IsApiResponseValid = <TData>(data: TData, schema: ZodSchema) => boolean;
+
+export const validateDataUsingZodSchema: IsApiResponseValid = (data, schema: ZodSchema) => {
+  const validationResults = schema.safeParse(data);
+
+  if (validationResults.success === false) {
+    // TODO - probably need to rething way of handling parsing errors, for now let's leave it to console.log
+    // eslint-disable-next-line no-console
+    console.error('Error on parsing data', validationResults.error);
+  }
+
+  return validationResults.success;
+};
-- 
GitLab