With the Angular CLI's `ng serve`, how do you forward `/api` requests to a backend running locally on another port, and why does that not help in production?
answer
- same origin as the dev server
- a file the serve target points to
- one star versus two stars
- dev server only, not ng build
basics
~20 sCreate a proxy file such as src/proxy.conf.json mapping /api/** to the backend's target URL, set proxyConfig on the serve target, and restart ng serve. Only the dev server reads it, so production needs its own reverse proxy or CORS.
solid answer
~40 sThe app calls relative URLs like `/api/orders`, so the browser sends them to the dev server's own origin, `http://localhost:4200`. A proxy file tells `@angular/build:dev-server` to forward matching paths server-side: `{ "/api/**": { "target": "http://localhost:3000", "secure": false } }`, wired in with `"proxyConfig": "src/proxy.conf.json"` under the `serve` target's options or `ng serve --proxy-config`. Because the browser only ever talks to one origin, there is no CORS error in development. Two traps: in the current dev server `/api/*` matches one path segment only, so use `/api/**` for nested paths, and edits to the file need a restart of `ng serve`. `proxyConfig` is a dev-server option; `ng build` output contains no proxy, so production needs the host or a reverse proxy to route `/api`, or a backend that allows the origin through CORS.
code
json · 14 lines{
"projects": {
"shop": {
"architect": {
"serve": {
"builder": "@angular/build:dev-server",
"options": {
"proxyConfig": "src/proxy.conf.json"
}
}
}
}
}
}go deeper
Recall the steps: a proxy file with a target, proxyConfig on the serve target, relative URLs in the app, restart ng serve.
Explain why a same-origin proxy avoids CORS, how glob keys match paths in the Vite-based dev server, and what pathRewrite does.
Show you mirror the dev proxy in production routing, and can diagnose a proxy that stopped forwarding after a builder migration.
Weigh one origin behind a reverse proxy against a separate API origin with CORS: cookies, latency, and who owns the routing.
## Why a proxy at all In development the Angular app is served by `ng serve` on `http://localhost:4200`, while the backend often runs on another port, say `http://localhost:3000`. If the app called `http://localhost:3000/api/orders` directly, the browser would treat it as a **cross-origin** request and block it unless the backend sent CORS headers. The proxy removes the problem: the app calls a **relative** URL, `/api/orders`, which goes to the dev server's own origin, and the dev server forwards it to the backend from the server side. The browser never sees a second origin. ```ts import { Injectable, inject } from '@angular/core'; import { HttpClient } from '@angular/common/http'; interface Order { id: number; total: number; } @Injectable({ providedIn: 'root' }) export class OrdersApi { private readonly http = inject(HttpClient); list() { return this.http.get<Order[]>('/api/orders'); } } ``` ## Setting it up 1. Create a proxy file, for example `src/proxy.conf.json`. 2. Map the paths to forward to a `target`. 3. Point the `serve` target at the file with the `proxyConfig` option, or pass `--proxy-config` on the command line. 4. Restart `ng serve`; the file is read at start-up, and changes need a restart. ```json { "/api/**": { "target": "http://localhost:3000", "secure": false } } ``` `secure: false` is only needed when the target is HTTPS with a certificate the proxy should not verify, such as a self-signed one on a local backend. ## How paths are matched in the current dev server `@angular/build:dev-server`, the default serve builder, is based on Vite. The Angular CLI converts glob keys into anchored regular expressions before handing them over: | Key | Matches | |---|---| | `/api/*` | `/api/users`, but not `/api/users/42` | | `/api/**` | `/api/users` and `/api/users/42` | | `^/api/.*` | a key starting with `^` is used as a regular expression as written | The older webpack-based `@angular-devkit/build-angular:dev-server` treated a plain `/api` key as covering all sub-paths, so a proxy file copied from an older project can silently stop forwarding nested routes after a migration. Using `/api/**` explicitly avoids the ambiguity. Other formats the CLI accepts: - **JavaScript files** (`.js`, `.mjs`, `.cjs`) that export the same object, useful for computed targets. - **JSON with comments**: the loader tolerates comments and trailing commas. - **The webpack array form** with a `context` list of paths, converted into the object form. - **`pathRewrite`** objects, converted into the rewrite function the Vite proxy expects, for when the backend does not expect the `/api` prefix. ## Verifying the proxy 1. In the browser's network panel the request should go to `http://localhost:4200/api/...`, never to the backend's port; if it shows the backend's port, the code uses an absolute URL and bypasses the proxy. 2. The backend's own log should show the forwarded request, with the rewritten path if `pathRewrite` is set. 3. A response containing the app's `index.html` means the key did not match and the dev server answered the request itself. 4. An error reported in the `ng serve` terminal for a matching path usually means the target is down or on the wrong port. A common convention is to keep the API base URL relative (`/api`) in every environment, so the same code runs behind the dev proxy and behind the production reverse proxy without per-environment URLs. ## Why it does nothing in production `proxyConfig` is an option of the **dev-server builder**. `ng build` writes static files (and, for server output, a server bundle), and none of them contain a proxy. After deployment, a relative `/api/orders` call goes to whatever serves the app, so something there must route it: - a **reverse proxy** or the hosting platform's rewrite rules sending `/api` to the backend, keeping one origin; - or an **absolute backend URL** chosen per environment plus **CORS** on the backend, which is a server-side setting Angular cannot change. Treat the proxy file as a mirror of the production routing, not a replacement for it. ## Common mistakes - Using `/api/*` and wondering why `/api/users/42` returns the dev server's `index.html`. - Editing the proxy file without restarting `ng serve`. - Calling `http://localhost:3000/...` from the app, which bypasses the proxy entirely. - Expecting CORS errors in production to be fixed by the proxy file.
- Your backend serves `/orders`, not `/api/orders`; how do you keep the `/api` prefix in the app?Add `"pathRewrite": { "^/api": "" }` to the proxy entry. The Angular CLI converts that webpack-style object into the rewrite function the Vite-based dev server uses, so `/api/orders` is forwarded as `/orders`. Mirror the same rewrite in the production reverse proxy.
- Nested calls such as `/api/orders/42` return the app's HTML while `/api/orders` works; what is wrong?The key is probably `/api/*`. In `@angular/build:dev-server` a single star matches one path segment, so deeper paths fall through to the dev server's fallback page. Change the key to `/api/**` and restart `ng serve`.
- Can the proxy file be JavaScript instead of JSON?Yes. `proxyConfig` accepts `.js`, `.mjs` and `.cjs` files exporting the configuration object, as either CommonJS or an ES module default export. That helps when the target comes from an environment variable. JSON files may also contain comments.
The dev server acts like a receptionist who takes every call on the front desk line and quietly transfers the ones for the API team; the caller never learns another number exists. In production the receptionist is gone unless you hire one.
saying these in an interview costs you the question
- The proxy file also routes /api calls in the production build.
- In the current dev server /api/* covers every nested path.
- Proxy file changes are picked up live without a restart.
- The proxy works even if the app calls the backend's absolute URL.
- Setting proxyConfig on the build target fixes CORS after deployment.