---
title: "How to run a frontend and an API together in one coding challenge"
description: "Define each part as a service in kendor.yaml with its own folder, commands and port, pick the preview service, and route paths like /api to the API."
updated: 2026-10-06
canonical: https://kendor.io/docs/environments/multi-service-apps
---

# How to run a frontend and an API together in one coding challenge
A challenge can run several processes side by side, such as a React frontend and an API. Give each
one an entry under `services` in `kendor.yaml` with its folder (`workDir`), commands and port, name
the one the preview shows in `preview.service`, and add `routing` so requests to `/api` reach the API
while everything else reaches the frontend. Every service runs in the same sandbox.

## A complete example

A repository with a Vite frontend in `web/` and an Express API in `api/`, backed by PostgreSQL:

```yaml file="kendor.yaml"
version: 1
runtime:
  type: node
  version: "22"
databases:
  - type: postgres
    version: "18"
    init: api/db/seed.sql
services:
  api:
    workDir: api
    install:
      commands:
        - npm ci
    dev:
      command: node --watch src/server.js
      port: 3000
      env:
        vars:
          APP_URL: routing.web.url
    test:
      command: node --test
  web:
    workDir: web
    install:
      commands:
        - npm ci
    dev:
      command: npm run dev -- --port $PORT
      port: 5173
      env:
        vars:
          VITE_API_URL: routing.api.url
    publicEnvPrefix: VITE_
preview:
  service: web
routing:
  - path: /api
    service: api
  - path: /
    service: web
kendor:
  gradeService: api
```

## How the services start

1. Databases start first.
2. The top-level `install` runs once at the workspace root, if there is one.
3. Each service runs its own `install` commands in its `workDir`, then its `dev` command.

Each service gets its own process window, named after the service, and shows up in the **Processes**
view where the candidate can stop, start and restart it.

### workDir

`workDir` is the folder a service's install, dev and test commands run in, relative to the
workspace root. Leave it out for a service at the root. It can't start with `/` or contain `..`.

### Install once or per service

For separate packages, put `npm ci` (or `pip install`, `composer install`) in each service's
`install`. For a monorepo with one lockfile at the root, such as a pnpm workspace, use the
top-level `install` instead, so dependencies install once:

```yaml file="kendor.yaml"
version: 1
runtime:
  type: node
  version: "22"
install:
  commands:
    - pnpm install --frozen-lockfile
services:
  api:
    workDir: apps/api
    dev:
      command: pnpm dev
      port: 3000
  web:
    workDir: apps/web
    dev:
      command: pnpm dev --port $PORT
      port: 5173
preview:
  service: web
routing:
  - path: /api
    service: api
  - path: /
    service: web
```

### Ports

Give every service that serves HTTP a `dev.port`. Kendor sets `PORT` to it and `HOST` to `0.0.0.0`
for that service's dev command, so `--port $PORT` keeps the two in sync. Two services can't use the
same port.

## The preview and routing

`preview.service` is the service the candidate's preview pane opens. It's required as soon as there
is more than one service.

`routing` maps path prefixes on the preview address to services. The longest matching prefix wins,
so with the routes above, `/api/products` goes to `api` and every other path goes to `web`. The path
isn't rewritten: the API receives `/api/products`, so mount its routes under `/api`. Because the
frontend and the API share one address, the browser makes same-origin requests and there's no CORS
to set up.

While any routed service is still starting, for example while the API runs its migrations, the
preview shows a short waiting page instead of loading a frontend whose first API calls would fail.

Any service with a port can also be opened on its own address from the **Processes** view.

## Pass one service's URL to another

A service shouldn't hardcode `localhost` URLs for the browser, because the browser reaches the
sandbox through its preview address. Use a reference in `dev.env.vars` instead:

| Reference | Resolves to |
|---|---|
| `routing.api.url` | The preview address plus the API's route, for example `https://<preview-address>/api` |
| `routing.web.url` | The preview address itself, since `web` is routed at `/`. The API above uses it as `APP_URL`, for links it builds back to the frontend. |
| `databases.postgres.url` | The PostgreSQL connection URL |
| `lit:<text>` | Fixed text |

Kendor resolves these before any service starts, so the order the services come up in doesn't
matter. A reference to a service that doesn't exist fails validation when you save.

For server-to-server calls inside the sandbox, `http://localhost:3000` still works.

### publicEnvPrefix

`publicEnvPrefix` records which variables are meant for browser code, such as `VITE_` for Vite.
Kendor stores it but doesn't act on it today; Vite itself only exposes `VITE_` variables to the
bundle.

## Vite frontends

Vite blocks requests for host names it doesn't know, which includes the sandbox's preview address.
The `@kendordev/vite` plugin fixes that and points hot reload at the right address. It only acts
inside a Kendor sandbox, so it's safe to commit.

When you upload a zip or import a repository with a `vite.config` at the root, Kendor adds the
plugin for you. For a Vite app in a subfolder, such as `web/` above, add it yourself:

```js file="web/vite.config.js"
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import kendor from '@kendordev/vite';

export default defineConfig({
  plugins: [react(), kendor()],
});
```

Install it with `npm install -D @kendordev/vite` and commit the updated lockfile.

## Tests across services

The **Tests** tab runs the preview service's `test.command` by default. When the tests live in
another service, as with `api` above, set `kendor.gradeService` to that service. To run the tests of
several services, list them in `kendor.gradeServices`; each runs in its own `workDir` and the results
appear as `service › test`.
