Vue-Python

Welding Frontend onto FastAPI

The story of how Vue saw the snake and fell in love.

FastAPI is one of those pieces of software that makes me suspicious because the Python backend story is too pleasant. Type hints in Flask/Express style handlers become parsing and validation. Async with auto reloads works without summoning an eldritch framework configuration manual. Throw in its OpenAPI schemas and auto reloads and you can go from git init to a respectable API before the coffee machine has finished its boot sequence.

Then you need a frontend.

Historically, this is where the neon-lit expressway abruptly ended at a concrete wall.

FastAPI is very good at APIs, but it it cannot really serve your frontend at site root, unless you wanted all your requests routed to StaticFiles. And SPA support is unheard of. You could serve files, render templates, or attach whatever machinery you wanted, but the last mile between a modern JavaScript frontend and the Python application was largely left as an exercise for the reader.

And that reader was increasingly me.

Meanwhile, in frontend land

I use Vue for this job. Vue lives in roughly the same arena as React and Svelte: component-based interfaces, reactive state, client-side routing, and the usual modern machinery for turning a pile of source files into something a browser can execute.

The fastapi-vue package can however be used with others too, so I am describing the contenders briefly.

React

React is the 800-pound gorilla. It has a colossal ecosystem, an answer for everything, and usually three additional answers arguing with the first one.

Its biggest practical advantage is simply gravity: lots of developers know it, lots of libraries target it, and lots of example code begins with npm install react. It is vastly more popular than Vue, which makes the relatively thin integration between React and FastAPI rather telling in itself.

Even the official FastAPI full-stack solution — first released just some weeks ago — is fundamentally a template repository: a preselected React stack you start from, rather than tooling that can walk into an existing Python application and wire a frontend into it.

Svelte

Svelte takes a more compiler-heavy approach. Instead of shipping quite as much framework machinery into the browser, it transforms components at build time. The result can still run without Node in production.

It requires a bit of trickery for correct reactivity, requiring $ in its custom language to indicate what needs an update. On the flipside, this custom language allows for shorter code than its competitors.

Vue

Vue sits in a place I rather like: enough structure to build substantial applications, but without making the HTML disappear under several geological layers of JavaScript. Reactivity with its Pinia store Just Works, and stays out of the way. And when you need that absolute last bit of performance, it integrates well with non-reactive structures. No complicated wrapping or useState and such boilerplate required.

And it runs fast, which is not the case with most React apps.

Its official create-vue tool also gives me something particularly useful here: I don’t have to invent my idea of what a Vue project should look like. It can interactively ask whether the application should use TypeScript, Vue Router, Pinia, Vitest, Playwright/Cypress, ESLint, Prettier and the rest of the usual equipment.

That becomes important later.

Vue-Svelte-React
Solving a minimal web form that frequently appears in applications: a dropdown to choose a record to edit with inputs for its fields. Svelte has the shortest code, but Vue is a plain browser index.html and really just equal length. And then comes React with too much work to fit in a screenshot. All the jobs are in React and this image shows why.

How do we actually deploy this thing?

Once I have FastAPI on one side and Vue, React or Svelte on the other, there are a few fundamentally different ways I can put the resulting creature on a server.

Option 1: surrender to Node

The obvious JavaScript-world answer is to put a JavaScript server in production too.

That makes plenty of sense if I actually want a Node backend. There is a genuine advantage to having one language and closely related tooling on both sides of the wire. If my team wants TypeScript everywhere, that is a perfectly coherent architecture.

I don’t.

I chose FastAPI because I want to write the backend in Python. Installing another runtime beside it just to deliver frontend assets feels like hiring a second chef to carry plates from the kitchen to the dining room.

image
Do I want gigabytes of node_modules and this on my production server? Nope.

There is also the small matter of the JavaScript dependency universe. It is too heavy, maintains little compatibility and is often broken. While nvm can easily install the correct Node version, it still takes a massive amount of space that hosted servers often don’t have.

Node is useful on my development machine. Vite is excellent. create-vue is excellent. The build ecosystem does its job extremely well.

That doesn’t mean I want any of it on the production machine.

Option 2: render everything on the server

At the opposite extreme I can skip the SPA entirely and generate HTML in Python.

This deserves more respect than it sometimes gets. Not every application needs a client-side application runtime, a router and several megabytes of supporting civilization.

Tools such as htmx can take server-rendered HTML surprisingly far by adding HTTP requests, swaps, transitions, WebSockets and server-sent events directly through HTML attributes. Combine with html5tagger to create those documents and HTML fragments on the server, and you’ll have a site that runs everywhere and that can be read by social media, search engines and agents alike.

For sites dominated by documents, forms and relatively simple interactions, I like this architecture. The browser requests something, Python produces HTML, and nobody needs a build pipeline capable of launching a Mars probe.

Vasanko.com is built on this method, only bolting on Vue for the admin interface where stronger interactivity is required. This CMS runs on FastAPI–Vue setup, but renders all the pages serverside in Python.

Option 3: run two servers

The common compromise is therefore:

  • FastAPI runs the API.
  • Vite or another JavaScript server runs the frontend
  • A reverse proxy puts both under one hostname.
  • Production now has two applications, two dependency stacks, two processes and another configuration file holding the marriage together.

This works but ain’t pretty.

The browser does not care whether app.js was emitted by Python, Node, Caddy, nginx or a sufficiently determined toaster. Once the frontend has been built, the result is static HTML, CSS, JavaScript, fonts and images.

So why keep the factory running after the product has already left the assembly line?

Build with JavaScript, run with Python

That became the central idea behind fastapi-vue-setup.

I want JavaScript tooling where it is actually useful:

  • create-vue to create the frontend
  • Vite for the development server
  • Instant hot reloads on changes
  • The normal Vue build on Node, Bun or Deno

And then I want it gone.

The production artifact should be a Python package containing an already-built frontend. Installing that package should not require Node, npm, Vite, Vue source code, or the gravitational anomaly traditionally stored under node_modules.

That is exactly how fastapi-vue-setup builds projects today: uv build runs the frontend build and includes the resulting assets inside the Python package. The generated Hatch configuration treats frontend-build as an artifact and hooks the source-distribution build, while limiting package contents to the Python package itself.

The distinction matters especially for an sdist. I don’t want a source distribution to mean “here is some Python, here is a complete Vue development tree, now please install Node and reconstruct the actual application yourself.” The frontend is already compiled before the distribution leaves my machine.

The package contains what it needs to run, not everything I happened to need to build it.

That leaves deployment looking wonderfully boring:

uvx my-app

No Node installation ceremony on the target machine. No npm install. No frontend server. No JavaScript dependency tree waking up three months later and asking to be fed.

The package starts FastAPI, and FastAPI serves the application.

The other ugly problem

Building the frontend was only half of it.

StaticFiles is not quite a frontend server

FastAPI exposes Starlette’s StaticFiles, and for ordinary static assets it does exactly what it says on the tin:

app.mount("/static", StaticFiles(directory="static"))

The trouble starts when the frontend is the site itself.

A Vue application normally wants /. It may also want /login, /settings, /dashboard/coffee-reactor/7, and anything else owned by the client-side router.

But mounting StaticFiles at / effectively hands that mounted application the entire remaining URL space. FastAPI’s own maintainer explained the problem years ago: mount it at the root and the static application takes over, so the normal path operations underneath it no longer work as expected.

And we need:

  • Frontend build at /*
  • FastAPI still handles
    • /api/...
    • /openapi.json
    • any arbitrary other routes

This is not the same problem as serving /static/logo.svg.

FastAPI has very recently improved its own frontend support, and current documentation now explicitly points frontend applications toward app.frontend() rather than plain StaticFiles. But fastapi-vue predates that solution and also has a somewhat different job: it is the small runtime half of this complete packaging system.

Runtime fastapi-vue

The companion fastapi-vue package provides a custom Frontend handler instead of mounting StaticFiles over the application.

In SPA mode it can return index.html for paths belonging to the client-side router rather than requiring a physical file with that name.

Note

Due to how FastAPI routing works, app mounts eat everything under that path. The rest of the routes are tried in order and the first one wins. Therefore in SPA mode we must place the catch-all last in the app module.

With SPA mode disabled it only binds to paths of actual files, allowing your routes after it still catch whatever falls through.

It also handles the less glamorous details I do not particularly feel like reimplementing every Tuesday:

  • ETag and Last-Modified
  • Immutable caching for built assets
  • RAM caching with zstd compression
  • SPA routing fallback and /favicon.ico when needed
  • Don’t serve accidental stale build in devmode

This is the production runtime dependency. It is tiny and Python-only.

All the machinery for building and developing the Vue application stays in the source project.

Enter fastapi-vue-setup

With those pieces in place, fastapi-vue-setup is mostly concerned with removing the repetitive wiring.

The basic command is deliberately unexciting: point it to your app folder, or . if you’re already there.

uvx fastapi-vue-setup my-app

But there are two rather different cases hiding behind it.

And it provides support utilities for the CLI entry point your app gets, which can then also take your own commandline options, something that fastapi run cannot provide.

Need to change the port numbers your prod and vite and dev backend use by default? Run with --ports. Without that it retains the ports you previously had configured. And you can always --listen on your CLI or scripts/devserver.py to change that at runtime.

Creating a new application

For a new project I don’t want fastapi-vue-setup to impose a frozen Vue template of my own making. I want it to create a Python project configured with the name I gave it, and the Vue setup I choose:

  • JavaScript or TypeScript
  • Vue Router or no router
  • Pinia or not
  • Testing, linting and formatting choices
  • The other options supported by the current create-vue

The tool then builds the FastAPI integration around the application the developer actually chose.

This matters because a template inevitably fossilizes someone’s preferences. Six months later its idea of a modern full stack may already belong in a museum display, and you are stuck with what you got.

I’d rather have the choice.

Setup complete

Already have an app?

Here I may already have a real project containing code I would prefer not to vaporize. It can be created with the same script or stand alone, and the script will patch it where needed.

The setup script detects the Python project and backend module, detects or creates the Vue frontend, and patches the pieces it can safely integrate with minimal changes.

Note

Move your existing Vue completely under frontend/ first. We place Vue there to avoid polluting the root with Node things. Place it there to have the script patch the existing app rather than create a new one.

Need to upgrade to latest version? Just run fastapi-vue-setup again and it will patch in what new features it got.

That is one of the major differences between this and a template repository.

A template says:
Fork this repository to start your project

I needed tooling that can also say:
Fine, you’re already 30,000 lines in. Show me where the patient is.

Your app is ready to run

uv run scripts/devserver.py

vite.config.js

Configure which paths are proxied to the backend. By default this just /api. This configuration only affects the dev setup, in production everything lands on FastAPI.

JS_RUNTIME

Environment with value bun/deno/node or path to one of them (otherwise we find one).

That launches the Vue/Vite development server and FastAPI with reload support. The generated Vite integration proxies backend requests to the Python development server. Your browser connects to Vite.

So during development I still get the things I actually like about the JavaScript ecosystem.

Hello world connecting FastAPI

When development is finished:

uv build  # Installs and (re)builds everything
my-app    # CLI entry point provided (in .venv)

Vue is compiled, the result is baked into the Python distribution, and the build machinery has completed its mission. You may uv publish your package if you so desire, or just copy over to prod from dist/, then install and run with:

uv tool install my-app-0.1.0.tar.gz
my-app

All I needed to install beforehand was UV and on the dev system Node itself. I didn’t need to touch npm at all and now I can install or run it somewhere else without carrying the JavaScript workshop along for the ride.

One application, finally

What I ultimately wanted was not particularly exotic.

  • I wanted to write the backend in Python.
  • I wanted to write the frontend in Vue.
  • I wanted Vite while developing it.

And I wanted to setup and deploy one thing quickly.

Not a Python application plus a JavaScript application. Not two containers joined together by nginx and mutual suspicion.

A <1MB Python package that Just Works™.

It contains the frontend. FastAPI serves it from the site root without swallowing the rest of the application. Vue routing works when I want an SPA, and plain file routing allows my backend to keep catch-alls to itself, e.g. handling any pretty URL with server side rendered content like on this site. That in turn may use Vue in places it needs to, two worlds in perfect harmony.

The JavaScript toolchain does what a toolchain is supposed to do:

it builds the software, then gets out of the way.