> ## Documentation Index
> Fetch the complete documentation index at: https://developer.upsun.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Define routes for your multiple apps

> Learn about the many ways you can define routes between your apps.

export const DynamicCodeBlock = ({language = 'yaml', filename, icon, lines, wrap, expandable, highlight, focus, children}) => {
  const STORAGE_KEY = 'upsun_versions_cache';
  const COMPOSABLE_STORAGE_KEY = 'upsun_composable_cache';
  const CACHE_TTL = 5 * 60 * 1000;
  const API_URL = 'https://meta.upsun.com/images';
  const COMPOSABLE_API_URL = 'https://meta.upsun.com/composable';
  const DEBUG_PREFIX = '[DynamicCodeBlock cache]';
  const [versionData, setVersionData] = useState(null);
  const [versionError, setVersionError] = useState(false);
  const [composableData, setComposableData] = useState(null);
  const [composableError, setComposableError] = useState(false);
  useEffect(() => {
    const fetchData = async () => {
      let cachedData = null;
      let cachedEtag = null;
      if (typeof localStorage !== 'undefined') {
        try {
          const cached = localStorage.getItem(STORAGE_KEY);
          if (cached) {
            const parsed = JSON.parse(cached);
            cachedData = parsed?.data || null;
            cachedEtag = parsed?.etag || null;
            if (cachedData && Date.now() - parsed.timestamp < CACHE_TTL) {
              return cachedData;
            }
          }
        } catch (err) {
          console.error('Failed to load from cache:', err);
        }
      }
      const requestHeaders = cachedEtag ? {
        'If-None-Match': cachedEtag
      } : {};
      console.debug(`${DEBUG_PREFIX} revalidating`, {
        storageKey: STORAGE_KEY,
        hasCachedData: Boolean(cachedData),
        hasCachedEtag: Boolean(cachedEtag)
      });
      const response = await fetch(API_URL, {
        headers: requestHeaders
      });
      if (response.status === 304 && cachedData) {
        console.debug(`${DEBUG_PREFIX} revalidated (304)`, {
          storageKey: STORAGE_KEY
        });
        if (typeof localStorage !== 'undefined') {
          try {
            const etag = response.headers.get('etag') || cachedEtag;
            localStorage.setItem(STORAGE_KEY, JSON.stringify({
              data: cachedData,
              etag,
              timestamp: Date.now()
            }));
          } catch (err) {
            console.error('Failed to refresh cache metadata:', err);
          }
        }
        return cachedData;
      }
      if (!response.ok) throw new Error(`API request failed: ${response.statusText}`);
      const data = await response.json();
      const etag = response.headers.get('etag');
      console.debug(`${DEBUG_PREFIX} refreshed (200)`, {
        storageKey: STORAGE_KEY,
        etag
      });
      if (typeof localStorage !== 'undefined') {
        try {
          localStorage.setItem(STORAGE_KEY, JSON.stringify({
            data,
            etag,
            timestamp: Date.now()
          }));
        } catch (err) {
          console.error('Failed to cache data:', err);
        }
      }
      return data;
    };
    fetchData().then(data => setVersionData(data)).catch(err => console.error('Failed to fetch version data:', err));
  }, []);
  const findHighestVersion = versionsMap => {
    if (!versionsMap || Object.keys(versionsMap).length === 0) return null;
    const entries = Object.entries(versionsMap);
    const active = entries.filter(([, v]) => v.upsun && v.upsun.status === 'supported' || v.upsun && v.upsun.status === 'deprecated');
    const candidates = active.length > 0 ? active : entries;
    let [highestName] = candidates[0];
    for (let i = 1; i < candidates.length; i++) {
      const [currentName] = candidates[i];
      const cp = currentName.split('.').map(Number);
      const hp = highestName.split('.').map(Number);
      for (let j = 0; j < Math.max(cp.length, hp.length); j++) {
        if ((cp[j] || 0) > (hp[j] || 0)) {
          highestName = currentName;
          break;
        } else if ((cp[j] || 0) < (hp[j] || 0)) {
          break;
        }
      }
    }
    return highestName;
  };
  const getVersion = (lang, requestedVersion = 'latest') => {
    if (lang === 'composable') {
      if (!composableData || !composableData.versions || Object.keys(composableData.versions).length === 0) return null;
      if (requestedVersion && requestedVersion !== 'latest') {
        return (requestedVersion in composableData.versions) ? requestedVersion : null;
      }
      return findHighestVersion(composableData.versions);
    }
    if (!versionData) return null;
    const imageData = versionData[lang];
    if (!imageData || !imageData.versions || Object.keys(imageData.versions).length === 0) {
      return null;
    }
    if (requestedVersion && requestedVersion !== 'latest') {
      return (requestedVersion in imageData.versions) ? requestedVersion : null;
    }
    return findHighestVersion(imageData.versions);
  };
  let code = typeof children === 'string' ? children : String(children || '');
  const codeLines = code.split('\n');
  while (codeLines.length > 0 && codeLines[0].trim() === '') codeLines.shift();
  while (codeLines.length > 0 && codeLines[codeLines.length - 1].trim() === '') codeLines.pop();
  if (codeLines.length > 0) {
    const indents = codeLines.filter(line => line.trim().length > 0).map(line => line.match(/^[ \t]*/)[0].length);
    const minIndent = Math.min(...indents);
    code = codeLines.map(line => line.slice(minIndent)).join('\n');
  }
  code = code.replace(/\{\{version:(.*?)\}\}/g, (match, params) => {
    const parts = params.split(':');
    const lang = parts[0];
    const ver = parts[1] || 'latest';
    const isComposable = lang === 'composable';
    const hasError = isComposable ? composableError : versionError;
    const dataReady = isComposable ? composableData !== null : versionData !== null;
    if (hasError) return '(unavailable)';
    if (dataReady) {
      const resolvedVersion = getVersion(lang, ver);
      return resolvedVersion || match;
    }
    return '...';
  });
  const codeBlockProps = {
    language,
    ...filename && ({
      filename
    }),
    ...icon && ({
      icon
    }),
    ...lines !== undefined && ({
      lines
    }),
    ...wrap !== undefined && ({
      wrap
    }),
    ...expandable !== undefined && ({
      expandable
    }),
    ...highlight && ({
      highlight
    }),
    ...focus && ({
      focus
    })
  };
  return <CodeBlock {...codeBlockProps}>{code}</CodeBlock>;
};

When you set up a project containing multiple applications,
all of your apps are served by a single [router for the project](/docs/routes).
Each of your apps must have a `name` that's unique within the project.
To define specific routes for one of your apps, use this `name`.

There are various ways you can define routes for multiple app projects.

In this project, you have a CMS app, two frontend apps (one using Symfony and another using Gatsby),
and a Mercure Rocks server app, defined as follows:

<DynamicCodeBlock language="yaml" filename=".upsun/config.yaml">
  {`
      applications:
        admin:
          source:
            root: admin
          type: nodejs:{{version:nodejs:latest}}
        api:
          source:
            root: api
          type: php:{{version:php:latest}}
        gatsby:
          source:
            root: gatsby
          type: nodejs:{{version:nodejs:latest}}
        mercure:
          source:
            root: mercure/.config
          type: golang:{{version:golang:latest}}`
  }
</DynamicCodeBlock>

<Note>
  You don't need to define a route for each app in the repository.
  If an app isn't specified, then it isn't accessible to the web.
  One good example of defining an app with no route is when you [use Git submodules](/docs/configure-apps/multi-app/project-structure#split-your-code-source-into-multiple-git-submodule-repositories) and want to [use a source operation to update your submodules](/docs/development/submodules#update-submodules).

  You can also achieve the same thing by defining the app as a [`worker`](/docs/configure-apps/image-properties/workers).
</Note>

Depending on your needs, you could configure the router container
[using subdomains](#define-routes-using-subdomains) or using [subdirectories](#define-routes-using-subdirectories).

### Define routes using subdomains

You could define routes for your apps as follows:

```yaml .upsun/config.yaml theme={null}
routes:
  "https://mercure.{default}/":
    type: upstream
    upstream: "mercure:http"
  "https://{default}/":
    type: upstream
    upstream: "api:http"
```

So if your default domain is `example.com`, that means:

* `https://mercure.example.com/` is served by your Mercure Rocks app (`mercure`).
* `https://example.com/` is served by your Symfony frontend app (`api`).

<Note>
  Using a subdomain might [double your network traffic](https://www.nickolinger.com/blog/2021-08-04-you-dont-need-that-cors-request/),
  so consider using a path like `https://{default}/api` instead.
</Note>

### Define routes using subdirectories

Alternatively, you could define your routes as follows:

```yaml .upsun/config.yaml theme={null}
routes:
  "https://{default}/":
    type: upstream
    upstream: "api:http"
  "https://{default}/admin":
    type: upstream
    upstream: "admin:http"
```

Then you would need to configure each app's `web.locations` property to match these paths:

<DynamicCodeBlock language="yaml" filename=".upsun/config.yaml">
  {`
      applications:
        admin:
          source:
            root: admin
          type: nodejs:{{version:nodejs:latest}}
          ...
          web:
            locations:
              '/admin':
                passthru: '/admin/index.html'
                root: 'build'
                index:
                  - 'index.html'
        api:
          source:
            root: api
          type: php:{{version:php:latest}}
          ...
          web:
            locations:
              "/":
                passthru: "/index.php"
                root: "public"
                index:
                  - index.php

      routes:
        "https://{default}/":
          type: upstream
          upstream: "api:http"
        "https://{default}/admin":
          type: upstream
          upstream: "admin:http"`
  }
</DynamicCodeBlock>

So if your default domain is `example.com`, that means:

* `https://example.com/` is served by your Symfony frontend app (`api`).
* `https://example.com/admin` is served by your Admin app (`admin`).

Note that in this example, for the configuration of your `admin` app,
you need to add the URL suffix `/admin` as both an index in the `web.locations` and a value for the `passhtru` setting.

For a complete example, [go to this project on GitHub](https://github.com/platformsh-templates/bigfoot-multiapp/tree/submodules-root-subfolders-applications).
