Next.js and Symfony as a Full-Stack Architecture: The Decision Guide for Your Next Project
Wolf-Tech builds most client projects on the same combination: Next.js on the frontend, Symfony as the API backend. It is not the only way to build a SaaS product, and it is not the right choice for every team, but for a B2B application that needs to scale past a weekend prototype, the Next.js Symfony architecture holds up better than the alternatives we get asked to compare it against. This is the case for it, and the blueprint for putting it together.
Why not a pure PHP full stack
Symfony can render the frontend too. Twig templates, Stimulus for interactivity, maybe a sprinkle of Turbo for page transitions. It works, and for content-heavy sites or internal tools it is often the simpler choice.
Where it starts to strain is component-heavy UI: dashboards with live filtering, multi-step forms with conditional fields, anything that behaves more like an application than a document. Twig was not built for that kind of state management, and bolting a JavaScript framework onto server-rendered templates tends to produce two half-finished systems instead of one working one.
React's component ecosystem exists because this problem is common enough to justify a whole tooling category around it. TypeScript catches a category of bugs that PHP's type system, even with strict types enabled, does not reach across the request boundary into the browser. And Next.js gives you independent frontend deployment: you can ship a UI fix without touching the API, which matters once you have more than one person on the team.
Why not a pure Node.js full stack
The opposite question comes up just as often: if you are already writing TypeScript for the frontend, why not write the whole backend in Node too?
For a lot of projects, that is a reasonable answer. Where Symfony earns its place is in the backend's own complexity. Symfony's dependency injection container and its convention for organizing services scale well as a codebase grows past a handful of engineers, in a way that a loosely structured Express or Fastify app often does not without a lot of self-imposed discipline. Doctrine ORM handles complex relational data models, migrations, and query building with a maturity that most Node ORMs are still catching up to. And PHP's request lifecycle, a fresh process per request rather than one long-running event loop, gives you a cleaner isolation boundary for multi-tenant SaaS, where one tenant's slow query should not be able to stall requests from every other tenant sharing the process.
Neither of these arguments is absolute. A team that is exclusively TypeScript and wants to stay that way has a legitimate reason to pick a Node backend instead. But if the team already has PHP experience, or the product's data model is genuinely complex, Symfony on the backend is not a compromise. It is the stronger tool for that half of the job.
The integration architecture
The cleanest version of this pairing treats Symfony as a pure JSON API, either through API Platform or through custom controllers that return serialized responses, with no server-rendered views at all. Next.js is the only thing that produces HTML.
On the Next.js side, data fetching splits two ways. Server Components fetch directly from the Symfony API during the server render, which keeps API keys and internal endpoints off the client and lets the initial page load arrive with data already in it. Client components that need to refetch, paginate, or respond to user interaction after the page has loaded use React Query against the same API, which gives you caching and background refetching without hand-rolling it.
Authentication runs on JWT tokens issued by Symfony, typically via LexikJWTAuthenticationBundle, with the token stored in an httpOnly cookie rather than local storage so it is not reachable from client-side JavaScript. Next.js middleware checks the token on protected routes before the page even renders.
The type mismatch problem, where the API's shape drifts from what the frontend expects, gets solved once and then mostly stays solved: Symfony (via API Platform or nelmio/api-doc-bundle) generates an OpenAPI spec, and a code generation step turns that spec into TypeScript types for the Next.js side. Run it in CI and a backend field rename becomes a type error in the frontend build instead of a runtime bug that shows up in production three weeks later.
The deployment architecture
Next.js and Symfony deploy as separate containers, which is also what makes the independent-deployment benefit real rather than theoretical. A typical setup:
The Next.js container runs the app in standalone output mode behind Node's built-in server, or behind Nginx if you want a caching layer in front of it. The Symfony container runs PHP-FPM, usually behind Nginx as well, serving the API under something like api.yourapp.com or a /api path segment depending on how you want cookies and CORS to behave.
Both containers share a PostgreSQL instance for the application's data and a Redis instance for Symfony's cache, sessions, and Messenger transport if you are using async jobs. Nginx, or a load balancer in front of both containers, routes requests to the right service based on path or subdomain. This is the same pattern we use for custom software development projects that need to scale past a single VPS: each piece can be resized, redeployed, or replaced independently once it is not sharing a process with everything else.
The monorepo setup
Keeping both halves in one repository makes coordinated changes easier without merging the codebases into one deployable unit. A workable layout puts the Symfony app under /api and the Next.js app under /web, with Composer managing PHP dependencies inside /api and an npm workspace managing the Next.js side under /web. The OpenAPI type generation script runs from the repo root as a pre-build step, reading the spec from the Symfony app and writing types into the Next.js app's source tree.
CI runs both test suites on every pull request regardless of which side changed, since a backend change can break a frontend build through the generated types even when no frontend files were touched. Deployment stays split: a merge to main can trigger a Next.js deploy, a Symfony deploy, or both, depending on which paths changed.
Path-based CI triggers keep this from becoming slower than it needs to be. A GitHub Actions workflow that checks whether a commit touched /api before running PHPUnit, and separately whether it touched /web before running the Next.js test suite and build, means a pure frontend change is not waiting on a PHP test run it has no way of breaking, and vice versa. The one exception is the OpenAPI generation step, which should always run, since that is the part that catches the cross-boundary breakage the split pipelines would otherwise miss.
Local development benefits from the same split. A docker-compose.yaml at the repo root that spins up Postgres, Redis, the Symfony container with Xdebug enabled, and the Next.js dev server all at once means a new engineer can clone the repo and have a working environment in one command, without separately configuring a PHP environment and a Node environment on their own machine.
Is this the right stack for your project
If your team already knows PHP and you are building something with a real data model behind it, invoices, subscriptions, multi-tenant permissions, this combination lets you use Symfony for the part it is good at and Next.js for the part it is good at, without forcing either one to do a job it was not designed for. If your team is exclusively JavaScript and the backend is genuinely simple, a Node-only stack will get you there with less to learn.
If you are mid-decision on this, or already committed to one side and running into friction with the other, a tech stack strategy session is usually enough to map out whether the friction is a real architecture mismatch or a fixable implementation problem. Reach out at hello@wolf-tech.io or visit wolf-tech.io if you want a second opinion before you commit.

