diff --git a/package-lock.json b/package-lock.json
index 44313fee70d8443fbad41850d1e5c817c64dc95e..47c4cfdd0fd1a3a5c2b88cc9384d96bc42dc219e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3,6 +3,16 @@
   "requires": true,
   "lockfileVersion": 1,
   "dependencies": {
+    "@apidevtools/json-schema-ref-parser": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-8.0.0.tgz",
+      "integrity": "sha512-n4YBtwQhdpLto1BaUCyAeflizmIbaloGShsPyRtFf5qdFJxfssj+GgLavczgKJFa3Bq+3St2CKcpRJdjtB4EBw==",
+      "requires": {
+        "@jsdevtools/ono": "^7.1.0",
+        "call-me-maybe": "^1.0.1",
+        "js-yaml": "^3.13.1"
+      }
+    },
     "@babel/cli": {
       "version": "7.8.4",
       "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.8.4.tgz",
@@ -2104,6 +2114,11 @@
         }
       }
     },
+    "@jsdevtools/ono": {
+      "version": "7.1.2",
+      "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.2.tgz",
+      "integrity": "sha512-qS/a24RA5FEoiJS9wiv6Pwg2c/kiUo3IVUQcfeM9JvsR6pM8Yx+yl/6xWYLckZCT5jpLNhslgjiA8p/XcGyMRQ=="
+    },
     "@loaders.gl/3d-tiles": {
       "version": "2.0.4",
       "resolved": "https://registry.npmjs.org/@loaders.gl/3d-tiles/-/3d-tiles-2.0.4.tgz",
@@ -2980,6 +2995,11 @@
         "svg.select.js": "^3.0.1"
       }
     },
+    "append-field": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
+      "integrity": "sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY="
+    },
     "aproba": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
@@ -2998,7 +3018,6 @@
       "version": "1.0.10",
       "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
       "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
-      "dev": true,
       "requires": {
         "sprintf-js": "~1.0.2"
       }
@@ -3483,7 +3502,6 @@
       "version": "1.5.0",
       "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
       "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
-      "dev": true,
       "optional": true,
       "requires": {
         "file-uri-to-path": "1.0.0"
@@ -3756,6 +3774,38 @@
       "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=",
       "dev": true
     },
+    "busboy": {
+      "version": "0.2.14",
+      "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz",
+      "integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=",
+      "requires": {
+        "dicer": "0.2.5",
+        "readable-stream": "1.1.x"
+      },
+      "dependencies": {
+        "isarray": {
+          "version": "0.0.1",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+          "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
+        },
+        "readable-stream": {
+          "version": "1.1.14",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
+          "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
+          "requires": {
+            "core-util-is": "~1.0.0",
+            "inherits": "~2.0.1",
+            "isarray": "0.0.1",
+            "string_decoder": "~0.10.x"
+          }
+        },
+        "string_decoder": {
+          "version": "0.10.31",
+          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+          "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
+        }
+      }
+    },
     "bytes": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
@@ -3824,6 +3874,11 @@
         "unset-value": "^1.0.0"
       }
     },
+    "call-me-maybe": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz",
+      "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms="
+    },
     "callsites": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -4876,6 +4931,16 @@
       "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.10.0.tgz",
       "integrity": "sha512-EhfEKevYGWhWlZbNeplfhIU/+N+x0iCIx7VzKlXma2EdQyznVlZhCptXUY+BegNpPW2kjdx15Rvq503YcXXrcA=="
     },
+    "deasync": {
+      "version": "0.1.20",
+      "resolved": "https://registry.npmjs.org/deasync/-/deasync-0.1.20.tgz",
+      "integrity": "sha512-E1GI7jMI57hL30OX6Ht/hfQU8DO4AuB9m72WFm4c38GNbUD4Q03//XZaOIHZiY+H1xUaomcot5yk2q/qIZQkGQ==",
+      "optional": true,
+      "requires": {
+        "bindings": "^1.5.0",
+        "node-addon-api": "^1.7.1"
+      }
+    },
     "debounce": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.0.tgz",
@@ -5123,6 +5188,38 @@
       "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==",
       "dev": true
     },
+    "dicer": {
+      "version": "0.2.5",
+      "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz",
+      "integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=",
+      "requires": {
+        "readable-stream": "1.1.x",
+        "streamsearch": "0.1.2"
+      },
+      "dependencies": {
+        "isarray": {
+          "version": "0.0.1",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+          "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
+        },
+        "readable-stream": {
+          "version": "1.1.14",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
+          "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
+          "requires": {
+            "core-util-is": "~1.0.0",
+            "inherits": "~2.0.1",
+            "isarray": "0.0.1",
+            "string_decoder": "~0.10.x"
+          }
+        },
+        "string_decoder": {
+          "version": "0.10.31",
+          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+          "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
+        }
+      }
+    },
     "diff-sequences": {
       "version": "25.2.1",
       "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-25.2.1.tgz",
@@ -6404,6 +6501,53 @@
         }
       }
     },
+    "express-openapi-validator": {
+      "version": "3.12.7",
+      "resolved": "https://registry.npmjs.org/express-openapi-validator/-/express-openapi-validator-3.12.7.tgz",
+      "integrity": "sha512-6r0vnuzv2sUOZZSj3fMSgWFN8CfiWZV6KXrBfm0AGsLKsyW41zUHTwASlnwDiF5z/TeLpH5w4rgisneG1G1fBw==",
+      "requires": {
+        "ajv": "^6.12.2",
+        "content-type": "^1.0.4",
+        "deasync": "^0.1.19",
+        "js-yaml": "^3.13.1",
+        "json-schema-ref-parser": "^8.0.0",
+        "lodash.merge": "^4.6.2",
+        "lodash.uniq": "^4.5.0",
+        "lodash.zipobject": "^4.1.3",
+        "media-typer": "^1.1.0",
+        "multer": "^1.4.2",
+        "ono": "^7.1.2",
+        "path-to-regexp": "^6.1.0"
+      },
+      "dependencies": {
+        "ajv": {
+          "version": "6.12.2",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz",
+          "integrity": "sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==",
+          "requires": {
+            "fast-deep-equal": "^3.1.1",
+            "fast-json-stable-stringify": "^2.0.0",
+            "json-schema-traverse": "^0.4.1",
+            "uri-js": "^4.2.2"
+          }
+        },
+        "fast-deep-equal": {
+          "version": "3.1.1",
+          "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz",
+          "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA=="
+        },
+        "media-typer": {
+          "version": "1.1.0",
+          "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
+          "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="
+        },
+        "path-to-regexp": {
+          "version": "6.1.0",
+          "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.1.0.tgz",
+          "integrity": "sha512-h9DqehX3zZZDCEm+xbfU0ZmwCGFCAAraPJWMXJ4+v32NjZJilVg3k1TcKsRgIb8IQ/izZSaydDc1OhJCZvs2Dw=="
+        }
+      }
+    },
     "expression-eval": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/expression-eval/-/expression-eval-2.1.0.tgz",
@@ -6639,7 +6783,6 @@
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
       "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
-      "dev": true,
       "optional": true
     },
     "filefy": {
@@ -11427,7 +11570,6 @@
       "version": "3.13.1",
       "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
       "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
-      "dev": true,
       "requires": {
         "argparse": "^1.0.7",
         "esprima": "^4.0.0"
@@ -11436,8 +11578,7 @@
         "esprima": {
           "version": "4.0.1",
           "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
-          "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
-          "dev": true
+          "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
         }
       }
     },
@@ -11521,6 +11662,14 @@
       "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
       "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM="
     },
+    "json-schema-ref-parser": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/json-schema-ref-parser/-/json-schema-ref-parser-8.0.0.tgz",
+      "integrity": "sha512-2P4icmNkZLrBr6oa5gSZaDSol/oaBHYkoP/8dsw63E54NnHGRhhiFuy9yFoxPuSm+uHKmeGxAAWMDF16SCHhcQ==",
+      "requires": {
+        "@apidevtools/json-schema-ref-parser": "8.0.0"
+      }
+    },
     "json-schema-traverse": {
       "version": "0.4.1",
       "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -11825,6 +11974,16 @@
       "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
       "integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ="
     },
+    "lodash.uniq": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
+      "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M="
+    },
+    "lodash.zipobject": {
+      "version": "4.1.3",
+      "resolved": "https://registry.npmjs.org/lodash.zipobject/-/lodash.zipobject-4.1.3.tgz",
+      "integrity": "sha1-s5n1q6j/YqdG9peb8gshT5ZNvvg="
+    },
     "loglevel": {
       "version": "1.6.7",
       "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.7.tgz",
@@ -12267,6 +12426,21 @@
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
       "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
     },
+    "multer": {
+      "version": "1.4.2",
+      "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.2.tgz",
+      "integrity": "sha512-xY8pX7V+ybyUpbYMxtjM9KAiD9ixtg5/JkeKUTD6xilfDv0vzzOFcCp4Ljb1UU3tSOM3VTZtKo63OmzOrGi3Cg==",
+      "requires": {
+        "append-field": "^1.0.0",
+        "busboy": "^0.2.11",
+        "concat-stream": "^1.5.2",
+        "mkdirp": "^0.5.1",
+        "object-assign": "^4.1.1",
+        "on-finished": "^2.3.0",
+        "type-is": "^1.6.4",
+        "xtend": "^4.0.0"
+      }
+    },
     "multicast-dns": {
       "version": "6.2.3",
       "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz",
@@ -12367,6 +12541,12 @@
         "lower-case": "^1.1.1"
       }
     },
+    "node-addon-api": {
+      "version": "1.7.1",
+      "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.1.tgz",
+      "integrity": "sha512-2+DuKodWvwRTrCfKOeR24KIc5unKjOh8mz17NCzVnHWfjAdDqbfbjqh7gUT+BkXBRQM52+xCHciKWonJ3CbJMQ==",
+      "optional": true
+    },
     "node-environment-flags": {
       "version": "1.0.6",
       "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.6.tgz",
@@ -12769,6 +12949,14 @@
         "mimic-fn": "^2.1.0"
       }
     },
+    "ono": {
+      "version": "7.1.2",
+      "resolved": "https://registry.npmjs.org/ono/-/ono-7.1.2.tgz",
+      "integrity": "sha512-es7Gfr+OGNFwiYpyHCLgBF+p/RA0qYbWysQKlZbLvvUBis5BygEs8TVJ4r+SgHDfagOgONhaAl6Y4JLy++0MTw==",
+      "requires": {
+        "@jsdevtools/ono": "7.1.2"
+      }
+    },
     "opn": {
       "version": "5.5.0",
       "resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz",
@@ -15243,8 +15431,7 @@
     "sprintf-js": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
-      "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
-      "dev": true
+      "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
     },
     "sshpk": {
       "version": "1.16.1",
@@ -15393,6 +15580,11 @@
       "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==",
       "dev": true
     },
+    "streamsearch": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz",
+      "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo="
+    },
     "string-length": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/string-length/-/string-length-3.1.0.tgz",
@@ -15637,6 +15829,19 @@
         "svg.js": "^2.6.5"
       }
     },
+    "swagger-ui-dist": {
+      "version": "3.25.1",
+      "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-3.25.1.tgz",
+      "integrity": "sha512-Sw/K95j1pT9TZtLKiHDEml7YqcXC9thTTQjxrvNgd9j1KzOIxpo/5lhHuUMAN/hxVAHetzmcBcQaBjywRXog8w=="
+    },
+    "swagger-ui-express": {
+      "version": "4.1.4",
+      "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.1.4.tgz",
+      "integrity": "sha512-Ea96ecpC+Iq9GUqkeD/LFR32xSs8gYqmTW1gXCuKg81c26WV6ZC2FsBSPVExQP6WkyUuz5HEiR0sEv/HCC343g==",
+      "requires": {
+        "swagger-ui-dist": "^3.18.1"
+      }
+    },
     "symbol-observable": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
@@ -17274,8 +17479,7 @@
     "xtend": {
       "version": "4.0.2",
       "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
-      "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
-      "dev": true
+      "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="
     },
     "y18n": {
       "version": "3.2.1",
diff --git a/package.json b/package.json
index 29843e37cc4bcfd76b162999c7679e62f286e2b1..cbb8981645b2ad6f583910e7b3b008523fcec0a6 100644
--- a/package.json
+++ b/package.json
@@ -62,7 +62,9 @@
     "deck.gl": "^8.0.17",
     "dotenv": "^8.2.0",
     "express": "^4.16.4",
+    "express-openapi-validator": "^3.12.7",
     "immutable": "^4.0.0-rc.12",
+    "js-yaml": "^3.13.1",
     "leaflet": "^1.6.0",
     "leaflet-draw": "^1.0.4",
     "leaflet-fullscreen": "^1.0.2",
@@ -93,6 +95,7 @@
     "rxjs": "^6.3.3",
     "rxjs-compat": "^6.3.3",
     "sass-loader": "^8.0.0",
+    "swagger-ui-express": "^4.1.4",
     "victory": "^0.26.1"
   },
   "standard": {
diff --git a/src/client/epics/index.js b/src/client/epics/index.js
index 5c5943ae020ad6dd40e9f12da3e508227b6b53f9..8550e1fa84245c279d48d94c9af177a8fff3cbd3 100644
--- a/src/client/epics/index.js
+++ b/src/client/epics/index.js
@@ -22,7 +22,7 @@ import {
   FETCH_PAGINATED_RESULTS,
   FETCH_PAGINATED_RESULTS_FAILED,
   FETCH_RESULTS,
-  FETCH_RESULTS_CLIENT_SIDE,
+  FETCH_FULL_TEXT_RESULTS,
   FETCH_RESULTS_FAILED,
   FETCH_BY_URI,
   FETCH_BY_URI_FAILED,
@@ -61,8 +61,8 @@ const port = window.location.hostname === 'localhost' || window.location.hostnam
   : ''
 
 export const apiUrl = (process.env.NODE_ENV === 'development')
-  ? `http://localhost:3001${rootUrl}/api/`
-  : `${window.location.protocol}//${window.location.hostname}${port}${rootUrl}/api/`
+  ? `http://localhost:3001${rootUrl}/api/v1`
+  : `${window.location.protocol}//${window.location.hostname}${port}${rootUrl}/api/v1`
 
 export const availableLocales = {
   en: localeEN,
@@ -84,16 +84,24 @@ const fetchPaginatedResultsEpic = (action$, state$) => action$.pipe(
       sortBy: sortBy,
       sortDirection: sortDirection
     })
-    const requestUrl = `${apiUrl}${resultClass}/paginated?${params}`
+    const requestUrl = `${apiUrl}/faceted-search/${resultClass}/paginated`
     // https://rxjs-dev.firebaseapp.com/api/ajax/ajax
-    return ajax.getJSON(requestUrl).pipe(
-      map(response => updatePaginatedResults({
-        resultClass: response.resultClass,
-        page: response.page,
-        pagesize: response.pagesize,
-        data: response.data,
-        sparqlQuery: response.sparqlQuery
-      })),
+    return ajax({
+      url: requestUrl,
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json'
+      },
+      body: params
+    }).pipe(
+      map(ajaxResponse =>
+        updatePaginatedResults({
+          resultClass: ajaxResponse.response.resultClass,
+          page: ajaxResponse.response.page,
+          pagesize: ajaxResponse.response.pagesize,
+          data: ajaxResponse.response.data,
+          sparqlQuery: ajaxResponse.response.sparqlQuery
+        })),
       // https://redux-observable.js.org/docs/recipes/ErrorHandling.html
       catchError(error => of({
         type: FETCH_PAGINATED_RESULTS_FAILED,
@@ -112,18 +120,25 @@ const fetchResultsEpic = (action$, state$) => action$.pipe(
   ofType(FETCH_RESULTS),
   withLatestFrom(state$),
   mergeMap(([action, state]) => {
-    const { resultClass, facetClass, groupBy } = action
+    const { resultClass, facetClass } = action
     const params = stateToUrl({
       facets: state[`${facetClass}Facets`].facets,
-      facetClass,
-      groupBy
+      facetClass
     })
-    const requestUrl = `${apiUrl}${resultClass}/all?${params}`
-    return ajax.getJSON(requestUrl).pipe(
-      map(response => updateResults({
+    const requestUrl = `${apiUrl}/faceted-search/${resultClass}/all`
+    // https://rxjs-dev.firebaseapp.com/api/ajax/ajax
+    return ajax({
+      url: requestUrl,
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json'
+      },
+      body: params
+    }).pipe(
+      map(ajaxResponse => updateResults({
         resultClass: resultClass,
-        data: response.data,
-        sparqlQuery: response.sparqlQuery
+        data: ajaxResponse.response.data,
+        sparqlQuery: ajaxResponse.response.sparqlQuery
       })),
       catchError(error => of({
         type: FETCH_RESULTS_FAILED,
@@ -138,52 +153,27 @@ const fetchResultsEpic = (action$, state$) => action$.pipe(
   })
 )
 
-const clientFSFetchResultsEpic = (action$, state$) => action$.pipe(
-  ofType(CLIENT_FS_FETCH_RESULTS),
-  withLatestFrom(state$),
-  debounceTime(500),
-  switchMap(([action, state]) => {
-    const { jenaIndex } = action
-    const selectedDatasets = pickSelectedDatasets(state.clientSideFacetedSearch.datasets)
-    const dsParams = selectedDatasets.map(ds => `dataset=${ds}`).join('&')
-    let requestUrl
-    if (action.jenaIndex === 'text') {
-      requestUrl = `${apiUrl}federatedSearch?q=${action.query}&${dsParams}`
-    } else if (action.jenaIndex === 'spatial') {
-      const { latMin, longMin, latMax, longMax } = state.leafletMap
-      requestUrl = `${apiUrl}federatedSearch?latMin=${latMin}&longMin=${longMin}&latMax=${latMax}&longMax=${longMax}&${dsParams}`
-    }
-    return ajax.getJSON(requestUrl).pipe(
-      map(response => clientFSUpdateResults({
-        results: response,
-        jenaIndex
-      })),
-      catchError(error => of({
-        type: CLIENT_FS_FETCH_RESULTS_FAILED,
-        error: error,
-        message: {
-          text: backendErrorText,
-          title: 'Error'
-        }
-      }))
-    )
-  })
-)
 const fetchResultCountEpic = (action$, state$) => action$.pipe(
   ofType(FETCH_RESULT_COUNT),
   withLatestFrom(state$),
   mergeMap(([action, state]) => {
     const { resultClass, facetClass } = action
     const params = stateToUrl({
-      facets: state[`${facetClass}Facets`].facets,
-      facetClass: facetClass
+      facets: state[`${facetClass}Facets`].facets
     })
-    const requestUrl = `${apiUrl}${resultClass}/count?${params}`
-    return ajax.getJSON(requestUrl).pipe(
-      map(response => updateResultCount({
-        resultClass: response.resultClass,
-        data: response.data,
-        sparqlQuery: response.sparqlQuery
+    const requestUrl = `${apiUrl}/faceted-search/${resultClass}/count`
+    return ajax({
+      url: requestUrl,
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json'
+      },
+      body: params
+    }).pipe(
+      map(ajaxResponse => updateResultCount({
+        resultClass: ajaxResponse.response.resultClass,
+        data: ajaxResponse.response.data,
+        sparqlQuery: ajaxResponse.response.sparqlQuery
       })),
       catchError(error => of({
         type: FETCH_RESULT_COUNT_FAILED,
@@ -198,22 +188,15 @@ const fetchResultCountEpic = (action$, state$) => action$.pipe(
   })
 )
 
-const fetchResultsClientSideEpic = (action$, state$) => action$.pipe(
-  ofType(FETCH_RESULTS_CLIENT_SIDE),
+const fullTextSearchEpic = (action$, state$) => action$.pipe(
+  ofType(FETCH_FULL_TEXT_RESULTS),
   withLatestFrom(state$),
   debounceTime(500),
   switchMap(([action, state]) => {
-    const searchUrl = apiUrl + 'search'
-    let requestUrl = ''
-    if (action.jenaIndex === 'text') {
-      requestUrl = `${searchUrl}?q=${action.query}`
-    } else if (action.jenaIndex === 'spatial') {
-      const { latMin, longMin, latMax, longMax } = state.map
-      requestUrl = `${searchUrl}?latMin=${latMin}&longMin=${longMin}&latMax=${latMax}&longMax=${longMax}`
-    }
+    const requestUrl = `${apiUrl}/full-text-search?q=${action.query}`
     return ajax.getJSON(requestUrl).pipe(
       map(response => updateResults({
-        resultClass: 'all',
+        resultClass: 'fullText',
         data: response.data,
         sparqlQuery: response.sparqlQuery,
         query: action.query,
@@ -221,7 +204,7 @@ const fetchResultsClientSideEpic = (action$, state$) => action$.pipe(
       })),
       catchError(error => of({
         type: FETCH_RESULTS_FAILED,
-        resultClass: 'all',
+        resultClass: 'fullText',
         error: error,
         message: {
           text: backendErrorText,
@@ -239,14 +222,21 @@ const fetchByURIEpic = (action$, state$) => action$.pipe(
     const { resultClass, facetClass, uri } = action
     const params = stateToUrl({
       facets: facetClass == null ? null : state[`${facetClass}Facets`].facets,
-      facetClass: facetClass
+      facetClass
     })
-    const requestUrl = `${apiUrl}${resultClass}/instance/${encodeURIComponent(uri)}?${params}`
-    return ajax.getJSON(requestUrl).pipe(
-      map(response => updateInstance({
+    const requestUrl = `${apiUrl}/${resultClass}/page/${encodeURIComponent(uri)}`
+    return ajax({
+      url: requestUrl,
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json'
+      },
+      body: params
+    }).pipe(
+      map(ajaxResponse => updateInstance({
         resultClass: resultClass,
-        data: response.data,
-        sparqlQuery: response.sparqlQuery
+        data: ajaxResponse.response.data,
+        sparqlQuery: ajaxResponse.response.sparqlQuery
       })),
       catchError(error => of({
         type: FETCH_BY_URI_FAILED,
@@ -274,14 +264,21 @@ const fetchFacetEpic = (action$, state$) => action$.pipe(
       sortBy: sortBy,
       sortDirection: sortDirection
     })
-    const requestUrl = `${apiUrl}${action.facetClass}/facet/${facetID}?${params}`
-    return ajax.getJSON(requestUrl).pipe(
-      map(res => updateFacetValues({
+    const requestUrl = `${apiUrl}/faceted-search/${action.facetClass}/facet/${facetID}`
+    return ajax({
+      url: requestUrl,
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json'
+      },
+      body: params
+    }).pipe(
+      map(ajaxResponse => updateFacetValues({
         facetClass: facetClass,
-        id: facetID,
-        data: res.data || [],
-        flatData: res.flatData || [],
-        sparqlQuery: res.sparqlQuery
+        id: ajaxResponse.response.id,
+        data: ajaxResponse.response.data || [],
+        flatData: ajaxResponse.response.flatData || [],
+        sparqlQuery: ajaxResponse.response.sparqlQuery
       })),
       catchError(error => of({
         type: FETCH_FACET_FAILED,
@@ -311,14 +308,21 @@ const fetchFacetConstrainSelfEpic = (action$, state$) => action$.pipe(
       sortDirection: sortDirection,
       constrainSelf: true
     })
-    const requestUrl = `${apiUrl}${action.facetClass}/facet/${facetID}?${params}`
-    return ajax.getJSON(requestUrl).pipe(
-      map(res => updateFacetValuesConstrainSelf({
+    const requestUrl = `${apiUrl}/faceted-search/${action.facetClass}/facet/${facetID}`
+    return ajax({
+      url: requestUrl,
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json'
+      },
+      body: params
+    }).pipe(
+      map(ajaxResponse => updateFacetValuesConstrainSelf({
         facetClass: facetClass,
         id: facetID,
-        data: res.data || [],
-        flatData: res.flatData || [],
-        sparqlQuery: res.sparqlQuery
+        data: ajaxResponse.response.data || [],
+        flatData: ajaxResponse.response.flatData || [],
+        sparqlQuery: ajaxResponse.response.sparqlQuery
       })),
       catchError(error => of({
         type: FETCH_FACET_FAILED,
@@ -334,6 +338,38 @@ const fetchFacetConstrainSelfEpic = (action$, state$) => action$.pipe(
   })
 )
 
+const clientFSFetchResultsEpic = (action$, state$) => action$.pipe(
+  ofType(CLIENT_FS_FETCH_RESULTS),
+  withLatestFrom(state$),
+  debounceTime(500),
+  switchMap(([action, state]) => {
+    const { jenaIndex } = action
+    const selectedDatasets = pickSelectedDatasets(state.clientSideFacetedSearch.datasets)
+    const dsParams = selectedDatasets.map(ds => `dataset=${ds}`).join('&')
+    let requestUrl
+    if (action.jenaIndex === 'text') {
+      requestUrl = `${apiUrl}/federated-search?q=${action.query}&${dsParams}`
+    } else if (action.jenaIndex === 'spatial') {
+      const { latMin, longMin, latMax, longMax } = state.leafletMap
+      requestUrl = `${apiUrl}/federated-search?latMin=${latMin}&longMin=${longMin}&latMax=${latMax}&longMax=${longMax}&${dsParams}`
+    }
+    return ajax.getJSON(requestUrl).pipe(
+      map(response => clientFSUpdateResults({
+        results: response,
+        jenaIndex
+      })),
+      catchError(error => of({
+        type: CLIENT_FS_FETCH_RESULTS_FAILED,
+        error: error,
+        message: {
+          text: backendErrorText,
+          title: 'Error'
+        }
+      }))
+    )
+  })
+)
+
 const loadLocalesEpic = action$ => action$.pipe(
   ofType(LOAD_LOCALES),
   // https://thecodebarbarian.com/a-beginners-guide-to-redux-observable
@@ -441,12 +477,12 @@ const fetchGeoJSONLayer = async (layerID, bounds) => {
 const rootEpic = combineEpics(
   fetchPaginatedResultsEpic,
   fetchResultsEpic,
-  clientFSFetchResultsEpic,
   fetchResultCountEpic,
-  fetchResultsClientSideEpic,
   fetchByURIEpic,
   fetchFacetEpic,
   fetchFacetConstrainSelfEpic,
+  fullTextSearchEpic,
+  clientFSFetchResultsEpic,
   loadLocalesEpic,
   fetchSimilarDocumentsEpic,
   fetchGeoJSONLayersEpic,
diff --git a/src/client/helpers/helpers.js b/src/client/helpers/helpers.js
index 2c5c0111c622bf7b46e8cbcb08c76f69f089bcf9..9671380876a38b13d173344346f712d73c5e53c5 100644
--- a/src/client/helpers/helpers.js
+++ b/src/client/helpers/helpers.js
@@ -22,45 +22,50 @@ export const stateToUrl = ({
   if (constrainSelf !== null) { params.constrainSelf = constrainSelf }
   if (groupBy !== null) { params.groupBy = groupBy }
   if (facets !== null) {
-    const constraints = {}
+    const constraints = []
     for (const [key, value] of Object.entries(facets)) {
       if (has(value, 'uriFilter') && value.uriFilter !== null) {
-        constraints[key] = {
+        constraints.push({
+          facetID: key,
           filterType: value.filterType,
           priority: value.priority,
           values: Object.keys(value.uriFilter)
-        }
+        })
       } else if (has(value, 'spatialFilter') && value.spatialFilter !== null) {
-        constraints[key] = {
+        constraints.push({
+          facetID: key,
           filterType: value.filterType,
           priority: value.priority,
           values: boundsToValues(value.spatialFilter._bounds)
-        }
+        })
       } else if (has(value, 'textFilter') && value.textFilter !== null) {
-        constraints[key] = {
+        constraints.push({
+          facetID: key,
           filterType: value.filterType,
           priority: value.priority,
           values: value.textFilter
-        }
+        })
       } else if (has(value, 'timespanFilter') && value.timespanFilter !== null) {
-        constraints[key] = {
+        constraints.push({
+          facetID: key,
           filterType: value.filterType,
           priority: value.priority,
           values: value.timespanFilter
-        }
+        })
       } else if (has(value, 'integerFilter') && value.integerFilter !== null) {
-        constraints[key] = {
+        constraints.push({
+          facetID: key,
           filterType: value.filterType,
           priority: value.priority,
           values: value.integerFilter
-        }
+        })
       }
     }
-    if (Object.keys(constraints).length > 0) {
-      params.constraints = JSON.stringify(constraints)
+    if (constraints.length > 0) {
+      params.constraints = constraints
     }
   }
-  return querystring.stringify(params)
+  return params
 }
 
 export const urlToState = ({ initialState, queryString }) => {
diff --git a/src/server/index.js b/src/server/index.js
index 5287e9328157162a44f6d63879b49e7473ba5ac6..9a98a5c3e3d3b207280c0cc52b21212852f2e945 100644
--- a/src/server/index.js
+++ b/src/server/index.js
@@ -1,3 +1,4 @@
+import fs from 'fs'
 import express from 'express'
 import path from 'path'
 import bodyParser from 'body-parser'
@@ -12,6 +13,9 @@ import { getFacet } from './sparql/FacetValues'
 import { queryJenaIndex } from './sparql/JenaQuery'
 import { getFederatedResults } from './sparql/FederatedSearch'
 import { fetchGeoJSONLayer } from './wfs/WFSApi'
+import swaggerUi from 'swagger-ui-express'
+import { OpenApiValidator } from 'express-openapi-validator'
+import yaml from 'js-yaml'
 const DEFAULT_PORT = 3001
 const app = express()
 app.set('port', process.env.PORT || DEFAULT_PORT)
@@ -20,10 +24,15 @@ app.use(bodyParser.json())
 // NODE_ENV is defined in package.json when running in localhost
 const isDevelopment = process.env.NODE_ENV === 'development'
 
-// allow CORS
+// CORS middleware
 app.use(function (req, res, next) {
   res.header('Access-Control-Allow-Origin', '*')
+  res.header('Access-Control-Allow-Methods', 'GET,POST,OPTIONS')
   res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept')
+  // handle pre-flight request
+  if (req.method === 'OPTIONS') {
+    return res.status(200).end()
+  }
   next()
 })
 
@@ -37,186 +46,190 @@ if (!isDevelopment) {
 }
 
 // React app makes requests to these api urls
-const apiPath = '/api'
-
-// https://medium.com/@Abazhenov/using-async-await-in-express-with-node-8-b8af872c0016
-app.get(`${apiPath}/:resultClass/paginated`, async (req, res, next) => {
-  try {
-    const data = await getPaginatedResults({
-      resultClass: req.params.resultClass,
-      page: req.query.page == null ? null : req.query.page,
-      pagesize: parseInt(req.query.pagesize) || null,
-      sortBy: req.query.sortBy || null,
-      sortDirection: req.query.sortDirection || null,
-      constraints: req.query.constraints == null ? null : JSON.parse(req.query.constraints),
-      resultFormat: req.query.resultFormat == null ? 'json' : req.query.resultFormat
-    })
-    res.json(data)
-  } catch (error) {
-    next(error)
-  }
+const apiPath = '/api/v1'
+
+// Generate API docs from YAML file with Swagger UI
+let swaggerDocument
+try {
+  swaggerDocument = yaml.safeLoad(fs.readFileSync(path.join(__dirname, './openapi.yaml'), 'utf8'))
+} catch (e) {
+  console.log(e)
+}
+
+app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument))
+
+new OpenApiValidator({
+  apiSpec: swaggerDocument,
+  validateResponses: true
 })
+  .install(app)
+  .then(() => {
+    // https://medium.com/@Abazhenov/using-async-await-in-express-with-node-8-b8af872c0016
+    app.post(`${apiPath}/faceted-search/:resultClass/paginated`, async (req, res, next) => {
+      const { params, body } = req
+      try {
+        const data = await getPaginatedResults({
+          resultClass: params.resultClass,
+          page: body.page,
+          pagesize: parseInt(body.pagesize),
+          sortBy: body.sortBy,
+          sortDirection: body.sortDirection,
+          constraints: body.constraints,
+          resultFormat: 'json'
+        })
+        res.json(data)
+      } catch (error) {
+        console.log(error)
+        next(error)
+      }
+    })
 
-app.get(`${apiPath}/:resultClass/all`, async (req, res, next) => {
-  try {
-    const resultFormat = req.query.resultFormat == null ? 'json' : req.query.resultFormat
-    const data = await getAllResults({
-      resultClass: req.params.resultClass,
-      facetClass: req.query.facetClass || null,
-      constraints: req.query.constraints == null ? null : JSON.parse(req.query.constraints),
-      resultFormat: resultFormat,
-      groupBy: req.query.groupBy === 'true'
+    app.post(`${apiPath}/faceted-search/:resultClass/all`, async (req, res, next) => {
+      const { params, body } = req
+      const resultFormat = 'json'
+      try {
+        const data = await getAllResults({
+          resultClass: params.resultClass,
+          facetClass: body.facetClass,
+          constraints: body.constraints,
+          resultFormat: resultFormat
+        })
+        if (resultFormat === 'csv') {
+          res.writeHead(200, {
+            'Content-Type': 'text/csv',
+            'Content-Disposition': 'attachment; filename=results.csv'
+          })
+          res.end(data)
+        } else {
+          res.json(data)
+        }
+      } catch (error) {
+        next(error)
+      }
     })
-    if (resultFormat === 'csv') {
-      res.writeHead(200, {
-        'Content-Type': 'text/csv',
-        'Content-Disposition': 'attachment; filename=results.csv'
-      })
-      res.end(data)
-    } else {
-      res.json(data)
-    }
-  } catch (error) {
-    next(error)
-  }
-})
 
-app.get(`${apiPath}/:resultClass/count`, async (req, res, next) => {
-  try {
-    const data = await getResultCount({
-      resultClass: req.params.resultClass,
-      constraints: req.query.constraints == null ? null : JSON.parse(req.query.constraints),
-      resultFormat: req.query.resultFormat == null ? 'json' : req.query.resultFormat
+    app.post(`${apiPath}/faceted-search/:resultClass/count`, async (req, res, next) => {
+      const { params, body } = req
+      try {
+        const data = await getResultCount({
+          resultClass: params.resultClass,
+          constraints: body.constraints,
+          resultFormat: 'json'
+        })
+        res.json(data)
+      } catch (error) {
+        next(error)
+      }
     })
-    res.json(data)
-  } catch (error) {
-    next(error)
-  }
-})
 
-app.get(`${apiPath}/:resultClass/instance/:uri`, async (req, res, next) => {
-  try {
-    const data = await getByURI({
-      resultClass: req.params.resultClass,
-      facetClass: req.query.facetClass || null,
-      constraints: req.query.constraints == null ? null : JSON.parse(req.query.constraints),
-      variant: req.query.variant || null,
-      uri: req.params.uri,
-      resultFormat: req.query.resultFormat == null ? 'json' : req.query.resultFormat
+    app.post(`${apiPath}/:resultClass/page/:uri`, async (req, res, next) => {
+      const { params, body } = req
+      try {
+        const data = await getByURI({
+          resultClass: params.resultClass,
+          uri: params.uri,
+          facetClass: body.facetClass,
+          constraints: body.constraints,
+          resultFormat: 'json'
+        })
+        console.log(data)
+        res.json(data)
+      } catch (error) {
+        next(error)
+      }
     })
-    res.json(data)
-  } catch (error) {
-    next(error)
-  }
-})
 
-app.get(`${apiPath}/:facetClass/facet/:id`, async (req, res, next) => {
-  try {
-    const data = await getFacet({
-      facetClass: req.params.facetClass,
-      facetID: req.params.id,
-      sortBy: req.query.sortBy || null,
-      sortDirection: req.query.sortDirection || null,
-      constraints: req.query.constraints == null ? null : JSON.parse(req.query.constraints),
-      resultFormat: req.query.resultFormat == null ? 'json' : req.query.resultFormat,
-      constrainSelf: req.query.constrainSelf || false
+    app.post(`${apiPath}/faceted-search/:facetClass/facet/:id`, async (req, res, next) => {
+      const { params, body } = req
+      try {
+        const data = await getFacet({
+          facetClass: params.facetClass,
+          facetID: params.id,
+          sortBy: body.sortBy,
+          sortDirection: body.sortDirection,
+          constraints: body.constraints,
+          resultFormat: 'json',
+          constrainSelf: body.constrainSelf
+        })
+        res.json(data)
+      } catch (error) {
+        next(error)
+      }
     })
-    res.json(data)
-  } catch (error) {
-    next(error)
-  }
-})
 
-app.get(`${apiPath}/search`, async (req, res, next) => {
-  let queryTerm = ''
-  let latMin = 0
-  let longMin = 0
-  let latMax = 0
-  let longMax = 0
-  if (has(req.query, 'q')) {
-    queryTerm = req.query.q
-  }
-  if (has(req.query, 'latMin')) {
-    latMin = req.query.latMin
-    longMin = req.query.longMin
-    latMax = req.query.latMax
-    longMax = req.query.longMax
-  }
-  try {
-    const data = await queryJenaIndex({
-      queryTerm: queryTerm,
-      latMin: latMin,
-      longMin: longMin,
-      latMax: latMax,
-      longMax: longMax,
-      resultFormat: req.query.resultFormat == null ? 'json' : req.query.resultFormat
+    app.get(`${apiPath}/full-text-search`, async (req, res, next) => {
+      try {
+        const data = await queryJenaIndex({
+          queryTerm: req.query.q,
+          resultFormat: 'json'
+        })
+        res.json(data)
+      } catch (error) {
+        next(error)
+      }
     })
-    res.json(data)
-  } catch (error) {
-    next(error)
-  }
-})
 
-app.get(`${apiPath}/federatedSearch`, async (req, res, next) => {
-  let queryTerm = ''
-  let latMin = 0
-  let longMin = 0
-  let latMax = 0
-  let longMax = 0
-  if (has(req.query, 'q')) {
-    queryTerm = req.query.q
-  }
-  if (has(req.query, 'latMin')) {
-    latMin = req.query.latMin
-    longMin = req.query.longMin
-    latMax = req.query.latMax
-    longMax = req.query.longMax
-  }
-  try {
-    const data = await getFederatedResults({
-      queryTerm,
-      latMin,
-      longMin,
-      latMax,
-      longMax,
-      datasets: castArray(req.query.dataset),
-      resultFormat: req.query.resultFormat == null ? 'json' : req.query.resultFormat
+    app.get(`${apiPath}/federated-search`, async (req, res, next) => {
+      let queryTerm = ''
+      let latMin = 0
+      let longMin = 0
+      let latMax = 0
+      let longMax = 0
+      if (has(req.query, 'q')) {
+        queryTerm = req.query.q
+      }
+      if (has(req.query, 'latMin')) {
+        latMin = req.query.latMin
+        longMin = req.query.longMin
+        latMax = req.query.latMax
+        longMax = req.query.longMax
+      }
+      try {
+        const data = await getFederatedResults({
+          queryTerm,
+          latMin,
+          longMin,
+          latMax,
+          longMax,
+          datasets: castArray(req.query.dataset),
+          resultFormat: req.query.resultFormat == null ? 'json' : req.query.resultFormat
+        })
+        res.json(data)
+      } catch (error) {
+        next(error)
+      }
     })
-    res.json(data)
-  } catch (error) {
-    next(error)
-  }
-})
 
-app.get(`${apiPath}/wfs`, async (req, res, next) => {
-  const layerIDs = castArray(req.query.layerID)
-  try {
-    const data = await Promise.all(layerIDs.map(layerID => fetchGeoJSONLayer({ layerID })))
-    res.json(data)
-  } catch (error) {
-    next(error)
-  }
-})
+    app.get(`${apiPath}/wfs`, async (req, res, next) => {
+      const layerIDs = castArray(req.query.layerID)
+      try {
+        const data = await Promise.all(layerIDs.map(layerID => fetchGeoJSONLayer({ layerID })))
+        res.json(data)
+      } catch (error) {
+        next(error)
+      }
+    })
 
-// Express server is used to serve the React app only in production
-if (!isDevelopment) {
-  /*  Routes are matched to a url in order of their definition
+    // Express server is used to serve the React app only in production
+    if (!isDevelopment) {
+      /*  Routes are matched to a url in order of their definition
       Redirect all the the rest for react-router to handle */
-  app.get('*', function (request, response) {
-    response.sendFile(path.join(publicPath, 'index.html'))
-  })
-}
+      app.get('*', function (request, response) {
+        response.sendFile(path.join(publicPath, 'index.html'))
+      })
+    }
 
-const servingInfo = isDevelopment
-  ? 'NODE_ENV=development, so Webpack serves the React app'
-  : `Static files (e.g. the React app) will be served from ${publicPath}`
+    const servingInfo = isDevelopment
+      ? 'NODE_ENV=development, so Webpack serves the React app'
+      : `Static files (e.g. the React app) will be served from ${publicPath}`
 
-const port = app.get('port')
+    const port = app.get('port')
 
-app.listen(port, () =>
-  console.log(`
-  Express server listening on port ${port}
-  API path is ${apiPath}
-  ${servingInfo}
-  `)
-)
+    app.listen(port, () =>
+      console.log(`
+        Express server listening on port ${port}
+        API path is ${apiPath}
+        ${servingInfo}
+      `)
+    )
+  })
diff --git a/src/server/openapi.yaml b/src/server/openapi.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..fb5baf1ae9ab37dac0dc8b236f29b0b6d6cc24a3
--- /dev/null
+++ b/src/server/openapi.yaml
@@ -0,0 +1,346 @@
+openapi: 3.0.3
+info:
+  title: Sampo-UI API
+  description: Description
+  version: 1.0.0
+servers:
+  - url: /api/v1
+paths:
+  /faceted-search/{resultClass}/paginated:
+    post:
+      summary: Return faceted search results with pagination
+      parameters:
+      - in: path
+        name: resultClass
+        schema: 
+          type: string
+          example: perspective1
+        required: true
+        description: The class of the results
+      requestBody:
+        required: true
+        content: 
+          application/json:
+            schema:
+              type: object
+              properties:
+                page:
+                  type: integer
+                  default: 0
+                  example: 0 
+                pagesize:
+                  type: integer
+                  default: 10
+                  example: 10
+                sortBy:
+                 type: string
+                 nullable: true
+                 default: null
+                 example: null
+                sortDirection:
+                  type: string 
+                  nullable: true
+                  default: null
+                  example: null
+                constraints:
+                  type: array
+                  items: 
+                    type: object
+                  nullable: true
+                  default: null
+                  example: null  
+      responses:
+        '200':   
+          description: Paginated search results
+          content:
+            application/json:
+              schema: 
+                type: object
+                properties: 
+                  data: 
+                    type: array
+                    items: 
+                      type: object
+                    description: Results as an array of objects
+                  page:
+                    type: integer
+                    description: The current page
+                  pagesize:
+                    type: integer
+                    description: Items per page
+                  resultClass:
+                    type: string
+                    description: The class of the results    
+                  sparqlQuery:
+                    type: string
+                    description: The SPARQL query that was used for the results
+  /faceted-search/{resultClass}/all:
+    post:
+      summary: Return all search results
+      parameters:
+      - in: path
+        name: resultClass
+        schema: 
+          type: string
+          example: perspective1
+        required: true
+        description: The class of the results
+      requestBody:
+        required: true
+        content: 
+          application/json:
+            schema:
+              type: object
+              properties:
+                constraints:
+                  type: array
+                  items: 
+                    type: object
+                  nullable: true
+                  default: null
+                  example: null  
+      responses:
+        '200':   
+          description: All search results
+          content:
+            application/json:
+              schema: 
+                type: object
+                properties: 
+                  data: 
+                    type: array
+                    items: 
+                      type: object
+                    description: Results as an array of objects
+                  sparqlQuery:
+                    type: string
+                    description: The SPARQL query that was used for the results
+  /faceted-search/{resultClass}/count:
+    post:
+      summary: Return the total count of the faceted search results
+      parameters:
+      - in: path
+        name: resultClass
+        schema: 
+          type: string
+          example: perspective1
+        required: true
+        description: The class of the results
+      requestBody:
+        required: true
+        content: 
+          application/json:
+            schema:
+               type: object  
+               properties: 
+                   constraints:
+                      type: array
+                      items: 
+                        type: object
+                      nullable: true
+                      default: null  
+                      example: null
+      responses:
+        '200':   
+          description: The total count of the faceted search results
+          content:
+            application/json:
+              schema: 
+                type: object
+                properties: 
+                  data: 
+                    type: integer
+                  sparqlQuery:
+                    type: string
+                  resultClass: 
+                    type: string  
+                  constraints:
+                    type: array
+                    nullable: true
+                    items: 
+                      type: object
+                    default: null          
+  /faceted-search/{facetClass}/facet/{id}:
+    post:
+      summary: Return values for a single facet
+      parameters:
+        - in: path
+          name: facetClass
+          schema: 
+            type: string
+          required: true
+          description: The class of the facet
+        - in: path
+          name: id
+          schema: 
+            type: string
+          required: true
+          description: The id of the facet
+      requestBody:
+        required: true
+        content: 
+          application/json:
+            schema:
+              type: object
+              properties:
+                sortBy:
+                 type: string
+                sortDirection:
+                  type: string 
+                constraints:
+                    type: array
+                    items: 
+                      type: object
+                    nullable: true
+                    default: null
+                constrainSelf:
+                  type: boolean
+                  default: false           
+      responses:
+        '200':   
+          description: Facet values
+          content:
+            application/json:
+              schema: 
+                type: object
+                properties: 
+                  data:
+                    oneOf:
+                      - type: array
+                        items: 
+                          type: object
+                      - type: object 
+                    description: Facet values as an array of objects (checkbox facets) 
+                      or as a single object (timespan facets).              
+                  flatData: 
+                    type: array
+                    items: 
+                      type: object
+                    description: Facet values as an array of objects with no hierarchy  
+                  id:
+                    type: string
+                    description: The id of facet
+                  sparqlQuery:
+                    type: string
+                    description: The SPARQL query that was used for the values of the facet                 
+  /{resultClass}/page/{uri}:
+    post: 
+      summary: Return information about a single resource
+      parameters:
+        - in: path
+          name: resultClass
+          schema: 
+            type: string
+          required: true
+          description: The class of the resource
+        - in: path
+          name: uri
+          schema: 
+            type: string
+          required: true
+          description: The URI of the resource
+      requestBody:
+        required: true
+        content: 
+          application/json:
+            schema:
+              type: object
+              properties:
+                facetClass:
+                 type: string
+                constraints:
+                    type: array
+                    items: 
+                      type: object
+                    nullable: true
+                    default: null
+      responses:
+        '200':
+          description: Information about a single resource
+          content:
+            application/json:
+              schema: 
+                type: object
+                properties: 
+                  data: 
+                    type: array
+                    items: 
+                      type: object
+                    description: An array containing one object describing the resource
+                  sparqlQuery:
+                    type: string
+                    description: The SPARQL query that was used for retrieving the metadata
+  /full-text-search:
+    get:
+      summary: Full text search
+      parameters:
+        - in: query
+          name: q
+          schema: 
+            type: string
+          required: true
+          description: The query string
+      responses:
+        '200':
+          description: Full text search results
+          content:
+            application/json:
+              schema: 
+                type: object
+                properties: 
+                  data: 
+                    type: array
+                    items: 
+                      type: object
+                    description: An array of objects
+                  sparqlQuery:
+                    type: string
+                    description: The SPARQL query that was used for retrieving the results    
+  /federated-search:
+    get:
+      summary: Federated search can be used for retrieving the initial result set for 
+        client-side faceted search.
+      parameters:
+        - in: query
+          name: dataset
+          schema:
+            type: array
+            items: 
+              type: string
+          explode: true
+          required: true
+        - in: query
+          name: q
+          schema: 
+            type: string  
+          description: The query string
+        - in: query
+          name: latMin
+          schema: 
+            type: number
+        - in: query
+          name: longMin
+          schema: 
+            type: number
+        - in: query
+          name: latMax
+          schema: 
+            type: number
+        - in: query
+          name: longMax
+          schema: 
+            type: number
+      responses:
+        '200':
+          description: Federated search results
+          content:
+            application/json:
+              schema: 
+                type: array
+                items:
+                  type: object
+                description: Search results from multiple SPARQL endpoints
+                      merged into a single array
+
+
+
diff --git a/src/server/sparql/FacetResults.js b/src/server/sparql/FacetResults.js
index 8594b7b3894fc10ce1e2d8e5901a0dbd54395f5e..506cc3955eda05858f2ad750cefbc596f418af50 100644
--- a/src/server/sparql/FacetResults.js
+++ b/src/server/sparql/FacetResults.js
@@ -1,5 +1,4 @@
 import { runSelectQuery } from './SparqlApi'
-import { runNetworkQuery } from './NetworkApi'
 import { prefixes } from './sampo/SparqlQueriesPrefixes'
 import {
   countQuery,
@@ -10,11 +9,8 @@ import {
   manuscriptPropertiesFacetResults,
   manuscriptPropertiesInstancePage,
   productionPlacesQuery,
-  productionCoordinatesQuery,
   lastKnownLocationsQuery,
-  migrationsQuery,
-  networkLinksQuery,
-  networkNodesQuery
+  migrationsQuery
 } from './sampo/SparqlQueriesPerspective1'
 import { workProperties } from './sampo/SparqlQueriesPerspective2'
 import { eventProperties, eventPlacesQuery } from './sampo/SparqlQueriesPerspective3'
@@ -26,7 +22,7 @@ import {
   allPlacesQuery
 } from './sampo/SparqlQueriesPlaces'
 import { facetConfigs, endpoint } from './sampo/FacetConfigsSampo'
-import { mapCount, mapPlaces, mapCoordinates } from './Mappers'
+import { mapCount, mapPlaces } from './Mappers'
 import { makeObjectList } from './SparqlObjectMapper'
 import { generateConstraintsBlock } from './Filters'
 
@@ -77,20 +73,15 @@ export const getAllResults = ({
       filterTarget = 'id'
       break
     case 'placesMsProduced':
-      q = groupBy ? productionPlacesQuery : productionCoordinatesQuery
+      q = productionPlacesQuery
       filterTarget = 'manuscripts'
-      mapper = groupBy ? mapPlaces : mapCoordinates
+      mapper = mapPlaces
       break
     case 'lastKnownLocations':
       q = lastKnownLocationsQuery
       filterTarget = 'manuscripts'
       mapper = mapPlaces
       break
-    case 'placesActors':
-      q = placesActorsQuery
-      filterTarget = 'actor__id'
-      mapper = mapPlaces
-      break
     case 'placesMsMigrations':
       q = migrationsQuery
       filterTarget = 'manuscript__id'
@@ -99,14 +90,6 @@ export const getAllResults = ({
       q = eventPlacesQuery
       filterTarget = 'event'
       break
-    case 'eventsByTimePeriod':
-      q = generateEventsByPeriodQuery({ startYear: 1600, endYear: 1620, periodLength: 10 })
-      filterTarget = 'event'
-      break
-    case 'manuscriptsNetwork':
-      q = networkLinksQuery
-      filterTarget = 'source'
-      break
   }
   if (constraints == null) {
     q = q.replace('<FILTER>', '# no filters')
@@ -119,15 +102,6 @@ export const getAllResults = ({
       facetID: null
     }))
   }
-  if (resultClass === 'manuscriptsNetwork') {
-    // console.log(prefixes + q)
-    return runNetworkQuery({
-      endpoint,
-      prefixes,
-      links: q,
-      nodes: networkNodesQuery
-    })
-  }
   // console.log(prefixes + q)
   return runSelectQuery({
     query: prefixes + q,
diff --git a/src/server/sparql/Filters.js b/src/server/sparql/Filters.js
index f1e35b754448376e521e4647fc7a2cd339de4180..8b876665b30187d70aed0d321e1d733d8f607153 100644
--- a/src/server/sparql/Filters.js
+++ b/src/server/sparql/Filters.js
@@ -20,11 +20,11 @@ export const hasPreviousSelectionsFromOtherFacets = (constraints, facetID) => {
 }
 
 export const getUriFilters = (constraints, facetID) => {
-  for (const [key, value] of Object.entries(constraints)) {
-    if (key === facetID && value.filterType === 'uriFilter') {
-      return value.values
+  constraints.forEach(facet => {
+    if (facet.facetID === facetID && facet.filter === 'uriFilter') {
+      return facet.values
     }
-  }
+  })
   return []
 }
 
@@ -36,27 +36,16 @@ export const generateConstraintsBlock = ({
   inverse,
   constrainSelf = false
 }) => {
-  // delete constraints[facetID];
   let filterStr = ''
-  const constraintsArr = []
   const skipFacetID = constrainSelf ? '' : facetID
-  for (const [key, value] of Object.entries(constraints)) {
-    if (key !== skipFacetID) {
-      constraintsArr.push({
-        id: key,
-        filterType: value.filterType,
-        priority: value.priority,
-        values: value.values
-      })
-    }
-  }
-  constraintsArr.sort((a, b) => a.priority - b.priority)
-  constraintsArr.map(c => {
+  const modifiedConstraints = constraints.filter(facet => facet.facetID !== skipFacetID)
+  modifiedConstraints.sort((a, b) => a.priority - b.priority)
+  modifiedConstraints.map(c => {
     switch (c.filterType) {
       case 'textFilter':
         filterStr += generateTextFilter({
           facetClass: facetClass,
-          facetID: c.id,
+          facetID: c.facetID,
           filterTarget: filterTarget,
           queryString: c.values,
           inverse: inverse
@@ -65,7 +54,7 @@ export const generateConstraintsBlock = ({
       case 'uriFilter':
         filterStr += generateUriFilter({
           facetClass: facetClass,
-          facetID: c.id,
+          facetID: c.facetID,
           filterTarget: filterTarget,
           values: c.values,
           inverse: inverse
@@ -74,7 +63,7 @@ export const generateConstraintsBlock = ({
       case 'spatialFilter':
         filterStr += generateSpatialFilter({
           facetClass: facetClass,
-          facetID: c.id,
+          facetID: c.facetID,
           filterTarget: filterTarget,
           values: c.values,
           inverse: inverse
@@ -84,7 +73,7 @@ export const generateConstraintsBlock = ({
       case 'dateFilter':
         filterStr += generateTimespanFilter({
           facetClass: facetClass,
-          facetID: c.id,
+          facetID: c.facetID,
           filterTarget: filterTarget,
           values: c.values,
           inverse: inverse
@@ -94,7 +83,7 @@ export const generateConstraintsBlock = ({
       case 'integerFilterRange':
         filterStr += generateIntegerFilter({
           facetClass: facetClass,
-          facetID: c.id,
+          facetID: c.facetID,
           filterTarget: filterTarget,
           values: c.values,
           inverse: inverse