mirror of
https://github.com/fastapi/full-stack-fastapi-template.git
synced 2026-11-03 12:29:42 +00:00
♻️ Serve frontend from FastAPI (#2393)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
588dfd46e5
commit
e80d92452e
@@ -1,26 +0,0 @@
|
||||
# Stage 0, "build-stage", based on Bun, to build and compile the frontend
|
||||
FROM oven/bun:1 AS build-stage
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json bun.lock /app/
|
||||
|
||||
COPY frontend/package.json /app/frontend/
|
||||
|
||||
WORKDIR /app/frontend
|
||||
|
||||
RUN bun install
|
||||
|
||||
COPY ./frontend /app/frontend
|
||||
ARG VITE_API_URL
|
||||
|
||||
RUN bun run build
|
||||
|
||||
|
||||
# Stage 1, based on Nginx, to have only the compiled app, ready for production with Nginx
|
||||
FROM nginx:1
|
||||
|
||||
COPY --from=build-stage /app/frontend/dist/ /usr/share/nginx/html
|
||||
|
||||
COPY ./frontend/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY ./frontend/nginx-backend-not-found.conf /etc/nginx/extra-conf.d/backend-not-found.conf
|
||||
@@ -17,5 +17,3 @@ WORKDIR /app/frontend
|
||||
RUN bun install
|
||||
|
||||
COPY ./frontend /app/frontend
|
||||
|
||||
ARG VITE_API_URL
|
||||
|
||||
+8
-7
@@ -15,7 +15,7 @@ bun run dev
|
||||
|
||||
* Then open your browser at http://localhost:5173/.
|
||||
|
||||
Notice that this live server is not running inside Docker, it's for local development, and that is the recommended workflow. Once you are happy with your frontend, you can build the frontend Docker image and start it, to test it in a production-like environment. But building the image at every change will not be as productive as running the local development server with live reload.
|
||||
Notice that this live server is not running inside Docker, it's for local development, and that is the recommended workflow. Once you are happy with your frontend, you can build the backend Docker image and start it, to test the production-like setup where FastAPI serves the built frontend at `http://localhost:8000`.
|
||||
|
||||
Check the file `package.json` to see other available options.
|
||||
|
||||
@@ -25,18 +25,19 @@ If you are developing an API-only app and want to remove the frontend, you can d
|
||||
|
||||
* Remove the `./frontend` directory.
|
||||
|
||||
* In the `compose.yml` file, remove the whole service / section `frontend`.
|
||||
* In the `backend/app/main.py` file, remove the `app.frontend()` call.
|
||||
|
||||
* In the `compose.override.yml` file, remove the whole service / section `frontend` and `playwright`.
|
||||
* In the `backend/Dockerfile` file, remove the frontend build stage and the `COPY --from=frontend-build` instruction.
|
||||
|
||||
* In the `compose.override.yml` file, remove the `playwright` service.
|
||||
|
||||
Done, you have a frontend-less (api-only) app. 🤓
|
||||
|
||||
---
|
||||
|
||||
If you want, you can also remove the `FRONTEND` environment variables from:
|
||||
If you want, you can also remove the `FRONTEND_HOST` environment variable from:
|
||||
|
||||
* `.env`
|
||||
* `./scripts/*.sh`
|
||||
|
||||
But it would be only to clean them up, leaving them won't really have any effect either way.
|
||||
|
||||
@@ -71,10 +72,10 @@ Notice that everytime the backend changes (changing the OpenAPI schema), you sho
|
||||
|
||||
## Using a Remote API
|
||||
|
||||
If you want to use a remote API, you can set the environment variable `VITE_API_URL` to the URL of the remote API. For example, you can set it in the `frontend/.env` file:
|
||||
By default, the built frontend uses the same origin as the FastAPI app. If you want to use a remote API while running the Vite development server, you can set the environment variable `VITE_API_URL` to the URL of the remote API. For example, you can set it in the `frontend/.env` file:
|
||||
|
||||
```env
|
||||
VITE_API_URL=https://api.my-domain.example.com
|
||||
VITE_API_URL=https://my-domain.example.com
|
||||
```
|
||||
|
||||
Then, when you run the frontend, it will use that URL as the base URL for the API.
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
location /api {
|
||||
return 404;
|
||||
}
|
||||
location /docs {
|
||||
return 404;
|
||||
}
|
||||
location /redoc {
|
||||
return 404;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
server {
|
||||
listen 80;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
try_files $uri /index.html =404;
|
||||
}
|
||||
|
||||
include /etc/nginx/extra-conf.d/*.conf;
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
import 'dotenv/config'
|
||||
|
||||
const baseURL = process.env.PLAYWRIGHT_BASE_URL ?? 'http://localhost:5173'
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
@@ -24,7 +26,7 @@ export default defineConfig({
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: 'http://localhost:5173',
|
||||
baseURL,
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
@@ -83,9 +85,11 @@ export default defineConfig({
|
||||
],
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: {
|
||||
command: 'bun run dev',
|
||||
url: 'http://localhost:5173',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
webServer: process.env.PLAYWRIGHT_BASE_URL
|
||||
? undefined
|
||||
: {
|
||||
command: 'bun run dev',
|
||||
url: baseURL,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ import { Toaster } from "./components/ui/sonner"
|
||||
import "./index.css"
|
||||
import { routeTree } from "./routeTree.gen"
|
||||
|
||||
OpenAPI.BASE = import.meta.env.VITE_API_URL
|
||||
OpenAPI.BASE = import.meta.env.VITE_API_URL ?? ""
|
||||
OpenAPI.TOKEN = async () => {
|
||||
return localStorage.getItem("access_token") || ""
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import { PasswordInput } from "@/components/ui/password-input"
|
||||
import useAuth, { isLoggedIn } from "@/hooks/useAuth"
|
||||
|
||||
const formSchema = z.object({
|
||||
username: z.email(),
|
||||
username: z.email({ message: "Invalid email address" }),
|
||||
password: z
|
||||
.string()
|
||||
.min(1, { message: "Password is required" })
|
||||
|
||||
@@ -25,7 +25,7 @@ import useCustomToast from "@/hooks/useCustomToast"
|
||||
import { handleError } from "@/utils"
|
||||
|
||||
const formSchema = z.object({
|
||||
email: z.email(),
|
||||
email: z.email({ message: "Invalid email address" }),
|
||||
})
|
||||
|
||||
type FormData = z.infer<typeof formSchema>
|
||||
|
||||
@@ -22,7 +22,7 @@ import useAuth, { isLoggedIn } from "@/hooks/useAuth"
|
||||
|
||||
const formSchema = z
|
||||
.object({
|
||||
email: z.email(),
|
||||
email: z.email({ message: "Invalid email address" }),
|
||||
full_name: z.string().min(1, { message: "Full Name is required" }),
|
||||
password: z
|
||||
.string()
|
||||
|
||||
Vendored
+1
-1
@@ -1,7 +1,7 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_API_URL: string
|
||||
readonly VITE_API_URL?: string
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
|
||||
@@ -56,13 +56,11 @@ test("User can reset password successfully using the link", async ({
|
||||
|
||||
const selector = 'a[href*="/reset-password?token="]'
|
||||
|
||||
let url = await page.getAttribute(selector, "href")
|
||||
|
||||
// TODO: update var instead of doing a replace
|
||||
url = url!.replace("http://localhost/", "http://localhost:5173/")
|
||||
const url = await page.getAttribute(selector, "href")
|
||||
const resetUrl = new URL(url!)
|
||||
|
||||
// Set the new password and confirm it
|
||||
await page.goto(url)
|
||||
await page.goto(`${resetUrl.pathname}${resetUrl.search}`)
|
||||
|
||||
await page.getByTestId("new-password-input").fill(newPassword)
|
||||
await page.getByTestId("confirm-password-input").fill(newPassword)
|
||||
@@ -110,11 +108,11 @@ test("Weak new password validation", async ({ page, request }) => {
|
||||
)
|
||||
|
||||
const selector = 'a[href*="/reset-password?token="]'
|
||||
let url = await page.getAttribute(selector, "href")
|
||||
url = url!.replace("http://localhost/", "http://localhost:5173/")
|
||||
const url = await page.getAttribute(selector, "href")
|
||||
const resetUrl = new URL(url!)
|
||||
|
||||
// Set a weak new password
|
||||
await page.goto(url)
|
||||
await page.goto(`${resetUrl.pathname}${resetUrl.search}`)
|
||||
await page.getByTestId("new-password-input").fill(weakPassword)
|
||||
await page.getByTestId("confirm-password-input").fill(weakPassword)
|
||||
await page.getByRole("button", { name: "Reset Password" }).click()
|
||||
|
||||
@@ -6,6 +6,10 @@ import { defineConfig } from "vite"
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
build: {
|
||||
outDir: "../backend/app/frontend",
|
||||
emptyOutDir: true,
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
|
||||
Reference in New Issue
Block a user