diff --git a/src/components/routing/loader.test.ts b/src/components/routing/loader.test.ts
index 7f377c37bdb195b003483c4171d22ad557bb8a49..f3456506befd429873f2e7472806a1995c4fcfc1 100644
--- a/src/components/routing/loader.test.ts
+++ b/src/components/routing/loader.test.ts
@@ -122,4 +122,18 @@ describe('createRequestForRoute', () => {
     )
     expect(result).toEqual({key: {requestKey: 'value'}})
   })
+  it('works with requestPath', () => {
+    const result = createRequestForRoute(
+      [{route: {requestPath: 'some/path'}} as unknown as RouteMatch],
+      () => ({requestKey: 'value'}),
+    )
+    expect(result).toEqual({some: {path: {requestKey: 'value'}}})
+  })
+  it('works with requestPath function', () => {
+    const result = createRequestForRoute(
+      [{route: {requestKey: () => 'some/path'}} as unknown as RouteMatch],
+      () => ({requestKey: 'value'}),
+    )
+    expect(result).toEqual({some: {path: {requestKey: 'value'}}})
+  })
 })
diff --git a/src/components/routing/loader.ts b/src/components/routing/loader.ts
index b04bd89010aecf92525fa0234682f5776d5e42af..c5c04bddd73477ca9ecc96edb5b6fb7ebd61e2b2 100644
--- a/src/components/routing/loader.ts
+++ b/src/components/routing/loader.ts
@@ -1,9 +1,9 @@
-import lodash from 'lodash'
 import {User} from 'oidc-client-ts'
 
 import {graphApi} from '../../utils/api'
 import {resolveAllMDefs} from '../../utils/metainfo'
 import {JSONObject} from '../../utils/types'
+import {assert} from '../../utils/utils'
 import {DefaultSearch, LoaderResult, RouteMatch} from './types'
 
 export class DoesNotExistError extends Error {
@@ -58,8 +58,33 @@ export function getResponseForRoute(
   return getResponseForPath(path, response)
 }
 
-export function getRequestKey(match: RouteMatch) {
-  if (!match.route.requestKey) {
+function getRequestPath(
+  match: RouteMatch,
+  parentPath?: string,
+): string | undefined {
+  if (match.route.requestPath) {
+    if (typeof match.route.requestPath === 'string') {
+      return match.route.requestPath
+    } else {
+      return match.route.requestPath({
+        ...match,
+        search: match.search as DefaultSearch,
+      })
+    }
+  }
+  const key = getRequestKey(match)
+  if (key === '') {
+    return parentPath
+  }
+  if (parentPath === undefined) {
+    return key
+  } else {
+    return `${parentPath}/${key}`
+  }
+}
+
+function getRequestKey(match: RouteMatch) {
+  if (match.route.requestKey === undefined) {
     return match.path
   }
   if (typeof match.route.requestKey === 'string') {
@@ -75,24 +100,32 @@ export function createRequestForRoute(
   match: RouteMatch[],
   getRequest: (match: RouteMatch, index: number) => JSONObject | undefined,
 ): JSONObject {
-  const request = {} as JSONObject
-  const lastIndexWithRequest = match.findLastIndex(getRequest)
-  match.slice(0, lastIndexWithRequest + 1).reduce((request, match, index) => {
-    const next = getRequest(match, index) || {}
-    const key = getRequestKey(match)
-    if (key === '') {
-      lodash.merge(request, next)
-      return request
-    }
-    if (request[key] !== undefined) {
-      lodash.merge(request[key], next)
-    } else {
-      request[key] = next
+  const requests: {[path: string]: JSONObject} = {}
+  let parentPath: string | undefined
+  match.forEach((match, index) => {
+    const request = getRequest(match, index)
+    const path = getRequestPath(match, parentPath)
+    if (request) {
+      assert(path !== undefined, 'Request for empty paths are not supported.')
+      requests[path] = request
     }
-    return request[key] as JSONObject
-  }, request)
+    parentPath = path
+  })
+
+  const rootRequest = {} as JSONObject
+  Object.entries(requests).forEach(([path, request]) => {
+    const segments = path.split('/')
+    let currentRequest: JSONObject = rootRequest
+    segments.forEach((key) => {
+      if (currentRequest[key] === undefined) {
+        currentRequest[key] = {}
+      }
+      currentRequest = currentRequest[key] as JSONObject
+    })
+    Object.assign(currentRequest, request)
+  })
 
-  return request
+  return rootRequest
 }
 
 export default function loader(
diff --git a/src/components/routing/types.ts b/src/components/routing/types.ts
index 6047a84f926a9368b740bcddaf7aabd88d186d8a..570860cb0259755649b4775a994882871d922dd8 100644
--- a/src/components/routing/types.ts
+++ b/src/components/routing/types.ts
@@ -262,6 +262,18 @@ export type Route<
     | string
     | ((locationData: RouteMatch<Request, Response, Search>) => string)
 
+  /**
+   * An optional path or function that produces a path. The path will
+   * replace the API path usually derived from the `path` property of the route
+   * and its parent routes. Similar to request key, but for the whole path.
+   *
+   * This applies to the whole route including all child routes.
+   * TODO Are there cases where this is bad? Should this be configurable?
+   */
+  requestPath?:
+    | string
+    | ((locationData: RouteMatch<Request, Response, Search>) => string)
+
   /**
    * An optional request or function that produces a request. The request
    * has to satisfy the type `Request`. The request is used to create