Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions core/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ library manually.

### Symfony

If you are starting a new project, the easiest way to get API Platform up is to install
[API Platform for Symfony](../symfony/index.md).
If you are starting a new project, the easiest way to get API Platform up is to use the CLI:
`api-platform my-project --framework=symfony`. See [API Platform for Symfony](../symfony/index.md)
for details.

It comes with the API Platform core library integrated with
[the Symfony framework](https://symfony.com), [the schema generator](../schema-generator/index.md),
Expand Down
11 changes: 11 additions & 0 deletions laravel/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,17 @@ cd my-api-platform-laravel-app

## Installing API Platform

> [!TIP] The API Platform CLI can automate all of the steps below. To scaffold a new Laravel project
> with API Platform already installed, run:
>
> ```console
> api-platform my-project --framework=laravel
> ```
>
> This detects the `laravel` installer if available, creates the project, requires
> `api-platform/laravel`, and runs `php artisan api-platform:install` for you. The manual steps
> below remain valid for adding API Platform to an existing Laravel project.

In your Laravel project, install the API Platform integration for Laravel:

```console
Expand Down
193 changes: 130 additions & 63 deletions symfony/caddy.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,120 @@
# Configuring the Caddy Web Server with Symfony

[The API Platform Symfony variant](index.md), when generated with Docker, is shipped with
[the Caddy web server](https://caddyserver.com). The build contains the
[Mercure](../core/mercure.md) and the [Vulcain](https://vulcain.rocks) Caddy modules.
When you scaffold a project with the [API Platform CLI](index.md), the Symfony application is built
on top of [`symfony-docker`](https://github.com/dunglas/symfony-docker), which ships
[the Caddy web server](https://caddyserver.com) running [FrankenPHP](https://frankenphp.dev). The
build contains the [Mercure](../core/mercure.md) and the [Vulcain](https://vulcain.rocks) Caddy
modules.

Caddy is positioned in front of the web API and of the Progressive Web App (PWA). It routes requests
to either service depending on the value of the `Accept` HTTP header or the path of the request.
The Caddyfile lives at `api/frankenphp/Caddyfile`.

Using the same domain to serve the API and the PWA
[improves performance by preventing unnecessary CORS preflight requests and encourages embracing the REST principles](https://dunglas.fr/2022/01/preventing-cors-preflight-requests-using-content-negotiation/).
## How the CLI Serves the API and the PWA

## Why `route {}` Is Required
By default the API and the Progressive Web App (PWA) are served **separately**:

- Caddy serves the **API** on `https://localhost`.
- When you scaffold with `--with-pwa`, the Next.js application runs **standalone** with `pnpm dev`
on `http://localhost:3000`. It calls the API cross-origin using the `NEXT_PUBLIC_API_ENTRYPOINT`
value written to `pwa/.env.local`, and the CLI installs and configures
[`nelmio/cors-bundle`](https://github.com/nelmio/NelmioCorsBundle) on the API so those
cross-origin requests are allowed.

This keeps the two applications independent and requires no Caddy configuration. If you prefer to
serve both on the **same domain** through Caddy — which
[improves performance by preventing unnecessary CORS preflight requests and encourages embracing the REST principles](https://dunglas.fr/2022/01/preventing-cors-preflight-requests-using-content-negotiation/)
— see
[Serving the API and the PWA on the Same Domain](#serving-the-api-and-the-pwa-on-the-same-domain)
below.

## The Shipped Caddyfile

Out of the box, `api/frankenphp/Caddyfile` routes every request to the PHP application. The relevant
part of the site block looks like this:

```caddy
{$SERVER_NAME:localhost} {
root /app/public
encode zstd br gzip

mercure {
# ...Mercure hub configuration...
}

vulcain

# Extra directives injected by the CLI (see "The Link Header" below)
{$CADDY_SERVER_EXTRA_DIRECTIVES}

@phpRoute {
not path /.well-known/mercure*
not file {path}
}
rewrite @phpRoute index.php

@frontController path index.php
php @frontController {
worker {
file ./public/index.php
}
}

file_server {
hide *.php
}
}
```

Any request that is not an existing static file and is not a Mercure subscription is rewritten to
`index.php` and handled by Symfony through the FrankenPHP worker.

## The `Link` Header

The CLI adds a Hydra + Mercure `Link` header to every response. Rather than editing the Caddyfile
directly, it injects the directive through the `CADDY_SERVER_EXTRA_DIRECTIVES` environment variable
in `api/compose.yaml`, inside a recipe block:

```yaml
###> api-platform/api-platform ###
CADDY_SERVER_EXTRA_DIRECTIVES:
'header ?Link `</docs.jsonld>; rel="http://www.w3.org/ns/hydra/core#apiDocumentation",
</.well-known/mercure>; rel="mercure"`'
###< api-platform/api-platform ###
```

The `?` prefix means the header is only set when not already present in the response — a PHP
response that sets its own `Link` header is not overwritten.

Setting it at the Caddy level serves two purposes:

1. **API discoverability**: every response advertises the Hydra API documentation URL, allowing
clients to auto-discover the API.
2. **Mercure subscription**: every response advertises the Mercure hub URL, so clients can subscribe
to real-time updates without any application code.

## Serving the API and the PWA on the Same Domain

If you want Caddy to serve both the API and the Next.js application on a single domain, you need to
forward HTML requests to the PWA and keep API requests on PHP. This is **not** configured by the
CLI; the steps below add it on top of a scaffolded project.

### 1. Make the PWA reachable from the Caddy container

Caddy runs inside the `php` container, so it must be able to reach the Next.js server. Either:

- run the PWA in a Docker service named `pwa` listening on port `3000` (then the upstream is
`pwa:3000`), or
- keep running `pnpm dev` on the host and target it with `host.docker.internal:3000`.

Declare the upstream as an environment variable for the `php` service in `api/compose.yaml`:

```yaml
services:
php:
environment:
PWA_UPSTREAM: pwa:3000
```

### 2. Wrap the routing directives in a `route {}` block

Caddy processes directives in a
[predefined global order](https://caddyserver.com/docs/caddyfile/directives#directive-order), not in
Expand All @@ -21,7 +125,8 @@ Next.js.

Wrapping the directives in a `route {}` block enforces **strict first-match-wins evaluation in file
order**. The first directive that matches a request wins, and Caddy stops evaluating the rest. This
is what makes the `@pwa` proxy check run before the PHP rewrite:
makes the `@pwa` proxy check run before the PHP rewrite. Replace the `@phpRoute … file_server`
section of the site block with:

```caddy
route {
Expand All @@ -34,16 +139,20 @@ route {

# 3. Run PHP for index.php
@frontController path index.php
php @frontController
php @frontController {
worker {
file ./public/index.php
}
}

# 4. Serve remaining static files
file_server { hide *.php }
}
```

## The `@pwa` Matcher
### 3. Define the `@pwa` matcher

The `@pwa` named matcher is a
Add a `@pwa` named matcher a
[CEL (Common Expression Language) expression](https://caddyserver.com/docs/caddyfile/matchers#expression)
that decides which requests are forwarded to the Next.js application:

Expand All @@ -62,7 +171,7 @@ that decides which requests are forwarded to the Next.js application:
The expression has three independent clauses joined by `||`. A request matches `@pwa` if **any**
clause is true.

### Clause 1: HTML requests that are not API paths
#### Clause 1: HTML requests that are not API paths

A browser navigating to any URL sends `Accept: text/html, */*`. This clause forwards those requests
to Next.js unless the path is known to be served by the API or carries an extension that API
Expand All @@ -79,7 +188,7 @@ Paths excluded from Next.js (handled by PHP instead):
| `/_profiler*`, `/_wdt*` | Symfony Web Debug Toolbar and Profiler |
| `*.json*`, `*.html`, `*.csv`, `*.yml`, `*.yaml`, `*.xml` | Content-negotiated formats served by the API |

### Clause 2: Next.js static assets and well-known files
#### Clause 2: Next.js static assets and well-known files

```caddy
path('/favicon.ico', '/manifest.json', '/robots.txt', '/sitemap*', '/_next*', '/__next*')
Expand All @@ -89,7 +198,7 @@ These paths are forwarded to Next.js unconditionally, regardless of the `Accept`
and `/__next/*` are the internal asset paths used by the Next.js runtime for JavaScript chunks, CSS,
images, and hot module replacement updates in development.

### Clause 3: React Server Components requests
#### Clause 3: React Server Components requests

```caddy
query({'_rsc': '*'})
Expand All @@ -100,57 +209,15 @@ Next.js uses the `_rsc` query parameter internally for
data fetching. These requests do not carry `text/html` in their `Accept` header, so they would miss
clause 1 without this dedicated check.

## The `Link` Header

```caddy
header ?Link `</docs.jsonld>; rel="http://www.w3.org/ns/hydra/core#apiDocumentation", </.well-known/mercure>; rel="mercure"`
```

This directive is placed at the **site block level**, outside the `route {}` block, so it applies to
every response regardless of whether it came from PHP or Next.js. The `?` prefix means the header is
only set when not already present in the response — a PHP response that sets its own `Link` header
is not overwritten.

Setting this at the Caddy level serves two purposes:

1. **API discoverability**: every response advertises the Hydra API documentation URL, allowing
clients to auto-discover the API.
2. **Mercure subscription**: every response advertises the Mercure hub URL, so clients can subscribe
to real-time updates without any application code.

The Next.js application does not need to set these headers itself — they arrive on every response
automatically.

## The `PWA_UPSTREAM` Environment Variable

```caddy
reverse_proxy @pwa http://{$PWA_UPSTREAM}
```

`PWA_UPSTREAM` is resolved at runtime from the container environment. In `compose.yaml` it is set to
`pwa:3000`, where `pwa` is the Docker Compose service name and `3000` is the default port of the
Next.js server.

When the `pwa` service is not running (for example in an API-only project), Caddy returns a
`502 Bad Gateway` for any request matching `@pwa`. To run without a Next.js frontend, comment out
that line in the Caddyfile:

```caddy
route {
# Comment the following line if you don't want Next.js to catch requests for HTML documents.
# In this case, they will be handled by the PHP app.
# reverse_proxy @pwa http://{$PWA_UPSTREAM}

@phpRoute { not path /.well-known/mercure*; not file {path} }
rewrite @phpRoute index.php
@frontController path index.php
php @frontController
file_server { hide *.php }
}
```
When the PWA upstream is unreachable, Caddy returns a `502 Bad Gateway` for any request matching
`@pwa`. To temporarily fall back to PHP-rendered HTML, comment out the `reverse_proxy @pwa` line
inside the `route {}` block.

## Adjusting the Routing Rules

The rules below assume you have enabled single-domain serving and therefore have a `@pwa` matcher to
tweak.

### Routing an admin path to PHP

If you use EasyAdmin, SonataAdmin, or a custom Symfony controller that serves HTML pages, add the
Expand Down
8 changes: 7 additions & 1 deletion symfony/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,10 @@ When the PWA and the admin are enabled, the installer creates a project director
`api/` subdirectory (the Symfony API) alongside a `pwa/` directory (the Next.js application) and an
`admin/` directory (the React-admin SPA). The rest of this tutorial assumes this layout.

By default the installer also writes `AGENTS.md` and `CLAUDE.md` instruction files so AI coding
agents (Claude Code, Cursor, GitHub Copilot, …) know how to work with API Platform in your project.
Pass `--no-with-agents` to skip them.

API Platform is shipped with a [Docker](https://docker.com) definition that makes it easy to get a
containerized development environment up and running. If you do not already have Docker on your
computer, it's the right time to [install it](https://docs.docker.com/get-docker/).
Expand Down Expand Up @@ -167,7 +171,9 @@ This starts the following services:

When generated with `--with-pwa`, the Next.js application lives in the sibling `pwa/` directory. It
is **not** part of the Docker Compose stack: you run it separately with its own development server
(see [A Next.js Web App](#a-nextjs-web-app) below).
(see [A Next.js Web App](#a-nextjs-web-app) below). To serve the API and the PWA on the same domain
through Caddy instead, see
[Configuring the Caddy Web Server](caddy.md#serving-the-api-and-the-pwa-on-the-same-domain).

The following components are available:

Expand Down
Loading