Initial commit: ElysiaJS backend + TanStack Start frontend + Traefik compose
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
commit
a346cf3a5c
17 changed files with 505 additions and 0 deletions
7
.env.example
Normal file
7
.env.example
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
# Copy to .env and adjust before `docker compose up -d`.
|
||||||
|
# Public hostnames (must have DNS A records pointing at the VPS).
|
||||||
|
APP_DOMAIN=test-app.emmanuelariasa.com
|
||||||
|
API_DOMAIN=test-app-api.emmanuelariasa.com
|
||||||
|
|
||||||
|
# CORS origin the backend will accept (usually the frontend URL).
|
||||||
|
FRONTEND_ORIGIN=https://test-app.emmanuelariasa.com
|
||||||
18
.gitignore
vendored
Normal file
18
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# build output
|
||||||
|
.output/
|
||||||
|
.vinxi/
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# generated by TanStack Router plugin
|
||||||
|
frontend/src/routeTree.gen.ts
|
||||||
|
|
||||||
|
# env / secrets
|
||||||
|
.env
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# logs & misc
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
160
README.md
Normal file
160
README.md
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
# test-app-stack
|
||||||
|
|
||||||
|
A minimal full-stack reference project:
|
||||||
|
|
||||||
|
- **Backend** — [ElysiaJS](https://elysiajs.com) running on [Bun](https://bun.sh). Exposes a small JSON API.
|
||||||
|
- **Frontend** — [TanStack Start](https://tanstack.com/start) (React 19, SSR via Vite) that calls the backend and renders the result.
|
||||||
|
- **Orchestration** — `docker-compose.yml` wiring both services behind an existing [Traefik](https://traefik.io) reverse proxy with automatic Let's Encrypt TLS.
|
||||||
|
|
||||||
|
```
|
||||||
|
test-app-stack/
|
||||||
|
├── backend/ # ElysiaJS (Bun)
|
||||||
|
│ ├── src/index.ts # API: / /api/health /api/hello
|
||||||
|
│ ├── package.json
|
||||||
|
│ ├── tsconfig.json
|
||||||
|
│ └── Dockerfile
|
||||||
|
├── frontend/ # TanStack Start (React, SSR)
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── router.tsx
|
||||||
|
│ │ └── routes/
|
||||||
|
│ │ ├── __root.tsx # HTML shell
|
||||||
|
│ │ └── index.tsx # Home page — fetches /api/hello
|
||||||
|
│ ├── vite.config.ts
|
||||||
|
│ ├── package.json
|
||||||
|
│ ├── tsconfig.json
|
||||||
|
│ └── Dockerfile
|
||||||
|
├── docker-compose.yml # both services + Traefik labels
|
||||||
|
├── .env.example
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API surface (backend)
|
||||||
|
|
||||||
|
| Method | Path | Description |
|
||||||
|
| ------ | -------------- | --------------------------------------------- |
|
||||||
|
| GET | `/` | Service banner / metadata |
|
||||||
|
| GET | `/api/health` | Health probe (`{ status: "healthy", ... }`) |
|
||||||
|
| GET | `/api/hello` | Demo payload consumed by the frontend |
|
||||||
|
|
||||||
|
The frontend's home page (`frontend/src/routes/index.tsx`) calls `GET /api/hello`
|
||||||
|
against the URL in `VITE_API_URL` and shows the response — this is the
|
||||||
|
front ↔ back connection.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Local development
|
||||||
|
|
||||||
|
Two terminals.
|
||||||
|
|
||||||
|
**Backend** (requires [Bun](https://bun.sh)):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
bun install
|
||||||
|
bun run dev # http://localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
**Frontend** (requires Node 22+):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm install
|
||||||
|
# point the UI at the local backend
|
||||||
|
echo 'VITE_API_URL=http://localhost:3000' > .env
|
||||||
|
npm run dev # http://localhost:3000 (use a different port if backend is on 3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
> Both default to port 3000. For local dev run the backend on another port, e.g.
|
||||||
|
> `PORT=3001 bun run dev`, and set `VITE_API_URL=http://localhost:3001`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Manual build & deploy (Docker + Traefik)
|
||||||
|
|
||||||
|
This stack does **not** ship its own Traefik. It attaches to the Traefik
|
||||||
|
instance and the external `web` Docker network already running on the VPS
|
||||||
|
(from the forgejo stack), and reuses its `le` cert resolver.
|
||||||
|
|
||||||
|
### 1. Prerequisites
|
||||||
|
|
||||||
|
- Docker + Docker Compose on the host.
|
||||||
|
- An existing Traefik container attached to an external network named `web`,
|
||||||
|
with a cert resolver named `le` and a `websecure` (:443) entrypoint.
|
||||||
|
- **DNS**: create `A` records for both hostnames pointing at the VPS IP:
|
||||||
|
- `test-app.emmanuelariasa.com` → frontend
|
||||||
|
- `test-app-api.emmanuelariasa.com` → backend
|
||||||
|
|
||||||
|
(Adjust the names via `.env`. Without DNS, Traefik can't issue certificates.)
|
||||||
|
|
||||||
|
### 2. Configure
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
# edit .env if you want different hostnames / CORS origin
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Build the images
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose build
|
||||||
|
```
|
||||||
|
|
||||||
|
`VITE_API_URL` is passed as a build arg to the frontend because Vite inlines it
|
||||||
|
into the client bundle at build time — rebuild the frontend if the API hostname
|
||||||
|
changes.
|
||||||
|
|
||||||
|
### 4. Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
docker compose ps
|
||||||
|
docker compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
Traefik picks up the containers via their labels and provisions TLS
|
||||||
|
automatically. Once certificates are issued:
|
||||||
|
|
||||||
|
- Frontend → https://test-app.emmanuelariasa.com
|
||||||
|
- Backend → https://test-app-api.emmanuelariasa.com/api/health
|
||||||
|
|
||||||
|
### 5. Update / redeploy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git pull
|
||||||
|
docker compose build
|
||||||
|
docker compose up -d # recreates only changed services
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Tear down
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose down # add -v to also drop the app-internal network/volumes
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How the pieces connect
|
||||||
|
|
||||||
|
```
|
||||||
|
Internet ──TLS──▶ Traefik (:443, existing)
|
||||||
|
│ Host(test-app.emmanuelariasa.com) → frontend:3000
|
||||||
|
│ Host(test-app-api.emmanuelariasa.com) → backend:3000
|
||||||
|
▼
|
||||||
|
web (external docker network)
|
||||||
|
├── frontend (TanStack Start SSR)
|
||||||
|
└── backend (ElysiaJS/Bun) ── app-internal ──▶ (future DB)
|
||||||
|
```
|
||||||
|
|
||||||
|
The browser loads the SSR'd frontend, then calls the backend directly over its
|
||||||
|
public HTTPS hostname (`VITE_API_URL`). CORS on the backend is restricted to
|
||||||
|
`FRONTEND_ORIGIN`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- `frontend/src/routeTree.gen.ts` is generated on first `vite dev`/`build` and is git-ignored.
|
||||||
|
- Bun lockfile (`bun.lock`) and npm lockfile (`package-lock.json`) are created on first install; commit them for reproducible builds.
|
||||||
|
- This repository is intentionally deploy-ready but **not auto-deployed** — run the steps above manually.
|
||||||
4
backend/.dockerignore
Normal file
4
backend/.dockerignore
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
node_modules
|
||||||
|
.git
|
||||||
|
.env
|
||||||
|
*.log
|
||||||
17
backend/Dockerfile
Normal file
17
backend/Dockerfile
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
# --- Backend: ElysiaJS on Bun ---
|
||||||
|
FROM oven/bun:1.1-alpine AS deps
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json bun.lock* ./
|
||||||
|
RUN bun install --production --frozen-lockfile || bun install --production
|
||||||
|
|
||||||
|
FROM oven/bun:1.1-alpine AS runtime
|
||||||
|
WORKDIR /app
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV PORT=3000
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY . .
|
||||||
|
EXPOSE 3000
|
||||||
|
# Lightweight healthcheck hitting the Elysia /api/health route.
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||||
|
CMD wget -qO- http://127.0.0.1:3000/api/health || exit 1
|
||||||
|
CMD ["bun", "run", "src/index.ts"]
|
||||||
17
backend/package.json
Normal file
17
backend/package.json
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"name": "test-app-backend",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "ElysiaJS (Bun) backend for test-app-stack",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "bun run --watch src/index.ts",
|
||||||
|
"start": "bun run src/index.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"elysia": "^1.1.27",
|
||||||
|
"@elysiajs/cors": "^1.1.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"bun-types": "^1.1.34"
|
||||||
|
}
|
||||||
|
}
|
||||||
35
backend/src/index.ts
Normal file
35
backend/src/index.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { Elysia, t } from "elysia";
|
||||||
|
import { cors } from "@elysiajs/cors";
|
||||||
|
|
||||||
|
const PORT = Number(process.env.PORT ?? 3000);
|
||||||
|
|
||||||
|
// CORS: allow the frontend origin. In production set FRONTEND_ORIGIN to the
|
||||||
|
// public frontend URL; defaults to "*" for local development convenience.
|
||||||
|
const FRONTEND_ORIGIN = process.env.FRONTEND_ORIGIN ?? "*";
|
||||||
|
|
||||||
|
const app = new Elysia()
|
||||||
|
.use(cors({ origin: FRONTEND_ORIGIN }))
|
||||||
|
.get("/", () => ({
|
||||||
|
service: "test-app-stack backend",
|
||||||
|
runtime: "ElysiaJS on Bun",
|
||||||
|
status: "ok",
|
||||||
|
}))
|
||||||
|
// Health probe used by Docker / Traefik / monitoring.
|
||||||
|
.get("/api/health", () => ({
|
||||||
|
status: "healthy",
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
}))
|
||||||
|
// Demo endpoint the frontend calls to prove the front<->back connection.
|
||||||
|
.get("/api/hello", ({ query }) => ({
|
||||||
|
message: `Hello${query.name ? `, ${query.name}` : ""} from ElysiaJS + Bun!`,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
}), {
|
||||||
|
query: t.Object({ name: t.Optional(t.String()) }),
|
||||||
|
})
|
||||||
|
.listen(PORT);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`🦊 Backend running at http://${app.server?.hostname}:${app.server?.port}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
export type App = typeof app;
|
||||||
13
backend/tsconfig.json
Normal file
13
backend/tsconfig.json
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"types": ["bun-types"],
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"noEmit": true
|
||||||
|
}
|
||||||
|
}
|
||||||
56
docker-compose.yml
Normal file
56
docker-compose.yml
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
# test-app-stack — frontend (TanStack Start) + backend (ElysiaJS/Bun)
|
||||||
|
# Designed to plug into the EXISTING Traefik instance already running on the
|
||||||
|
# VPS (the one from forgejo-stack). It therefore reuses the external `web`
|
||||||
|
# network and the `le` Let's Encrypt cert resolver. No Traefik service is
|
||||||
|
# defined here on purpose.
|
||||||
|
|
||||||
|
services:
|
||||||
|
backend:
|
||||||
|
build: ./backend
|
||||||
|
container_name: test-app-backend
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- PORT=3000
|
||||||
|
- FRONTEND_ORIGIN=${FRONTEND_ORIGIN:-https://test-app.emmanuelariasa.com}
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
- app-internal
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.docker.network=web"
|
||||||
|
- "traefik.http.routers.testapp-api.rule=Host(`${API_DOMAIN:-test-app-api.emmanuelariasa.com}`)"
|
||||||
|
- "traefik.http.routers.testapp-api.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.testapp-api.tls.certresolver=le"
|
||||||
|
- "traefik.http.services.testapp-api.loadbalancer.server.port=3000"
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: ./frontend
|
||||||
|
args:
|
||||||
|
# Baked into the client bundle at build time.
|
||||||
|
VITE_API_URL: "https://${API_DOMAIN:-test-app-api.emmanuelariasa.com}"
|
||||||
|
container_name: test-app-frontend
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- PORT=3000
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.docker.network=web"
|
||||||
|
- "traefik.http.routers.testapp.rule=Host(`${APP_DOMAIN:-test-app.emmanuelariasa.com}`)"
|
||||||
|
- "traefik.http.routers.testapp.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.testapp.tls.certresolver=le"
|
||||||
|
- "traefik.http.services.testapp.loadbalancer.server.port=3000"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
# Shared with Traefik + forgejo. Created already by the forgejo stack.
|
||||||
|
web:
|
||||||
|
external: true
|
||||||
|
# Private network for any future backend<->db traffic; not exposed.
|
||||||
|
app-internal:
|
||||||
|
driver: bridge
|
||||||
6
frontend/.dockerignore
Normal file
6
frontend/.dockerignore
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
node_modules
|
||||||
|
.output
|
||||||
|
.vinxi
|
||||||
|
.git
|
||||||
|
.env
|
||||||
|
*.log
|
||||||
19
frontend/Dockerfile
Normal file
19
frontend/Dockerfile
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
# --- Frontend: TanStack Start (SSR, Node runtime) ---
|
||||||
|
FROM node:22-alpine AS build
|
||||||
|
WORKDIR /app
|
||||||
|
# VITE_API_URL must be present at build time: Vite inlines it into the bundle.
|
||||||
|
ARG VITE_API_URL
|
||||||
|
ENV VITE_API_URL=${VITE_API_URL}
|
||||||
|
COPY package.json package-lock.json* ./
|
||||||
|
RUN npm ci || npm install
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM node:22-alpine AS runtime
|
||||||
|
WORKDIR /app
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV PORT=3000
|
||||||
|
# TanStack Start (Vite) emits a self-contained server bundle in .output.
|
||||||
|
COPY --from=build /app/.output ./.output
|
||||||
|
EXPOSE 3000
|
||||||
|
CMD ["node", ".output/server/index.mjs"]
|
||||||
23
frontend/package.json
Normal file
23
frontend/package.json
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"name": "test-app-frontend",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite dev --port 3000",
|
||||||
|
"build": "vite build",
|
||||||
|
"start": "node .output/server/index.mjs"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/react-router": "^1.95.0",
|
||||||
|
"@tanstack/react-start": "^1.95.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tanstack/router-plugin": "^1.95.0",
|
||||||
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"typescript": "^5.7.2",
|
||||||
|
"vite": "^6.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
18
frontend/src/router.tsx
Normal file
18
frontend/src/router.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { createRouter as createTanStackRouter } from "@tanstack/react-router";
|
||||||
|
import { routeTree } from "./routeTree.gen";
|
||||||
|
|
||||||
|
// routeTree.gen.ts is auto-generated by the TanStack Router plugin on the
|
||||||
|
// first `vite dev` / `vite build`. It is git-ignored — do not edit by hand.
|
||||||
|
export function createRouter() {
|
||||||
|
return createTanStackRouter({
|
||||||
|
routeTree,
|
||||||
|
defaultPreload: "intent",
|
||||||
|
scrollRestoration: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "@tanstack/react-router" {
|
||||||
|
interface Register {
|
||||||
|
router: ReturnType<typeof createRouter>;
|
||||||
|
}
|
||||||
|
}
|
||||||
40
frontend/src/routes/__root.tsx
Normal file
40
frontend/src/routes/__root.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import {
|
||||||
|
HeadContent,
|
||||||
|
Outlet,
|
||||||
|
Scripts,
|
||||||
|
createRootRoute,
|
||||||
|
} from "@tanstack/react-router";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
|
export const Route = createRootRoute({
|
||||||
|
head: () => ({
|
||||||
|
meta: [
|
||||||
|
{ charSet: "utf-8" },
|
||||||
|
{ name: "viewport", content: "width=device-width, initial-scale=1" },
|
||||||
|
{ title: "test-app-stack" },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
component: RootComponent,
|
||||||
|
});
|
||||||
|
|
||||||
|
function RootComponent() {
|
||||||
|
return (
|
||||||
|
<RootDocument>
|
||||||
|
<Outlet />
|
||||||
|
</RootDocument>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function RootDocument({ children }: { children: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<HeadContent />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{children}
|
||||||
|
<Scripts />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
47
frontend/src/routes/index.tsx
Normal file
47
frontend/src/routes/index.tsx
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/")({
|
||||||
|
component: Home,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Public URL of the backend, injected at build time via Vite.
|
||||||
|
// Falls back to localhost for `vite dev`.
|
||||||
|
const API_URL = import.meta.env.VITE_API_URL ?? "http://localhost:3000";
|
||||||
|
|
||||||
|
type Hello = { message: string; timestamp: string };
|
||||||
|
|
||||||
|
function Home() {
|
||||||
|
const [data, setData] = useState<Hello | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch(`${API_URL}/api/hello`)
|
||||||
|
.then((r) => {
|
||||||
|
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
||||||
|
return r.json() as Promise<Hello>;
|
||||||
|
})
|
||||||
|
.then(setData)
|
||||||
|
.catch((e) => setError(String(e)));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main style={{ fontFamily: "system-ui, sans-serif", padding: "2rem", maxWidth: 640, margin: "0 auto" }}>
|
||||||
|
<h1>test-app-stack</h1>
|
||||||
|
<p>TanStack Start frontend talking to an ElysiaJS (Bun) backend.</p>
|
||||||
|
|
||||||
|
<section style={{ marginTop: "1.5rem", padding: "1rem", border: "1px solid #ddd", borderRadius: 8 }}>
|
||||||
|
<h2 style={{ marginTop: 0 }}>Backend connection</h2>
|
||||||
|
<p><strong>API URL:</strong> <code>{API_URL}</code></p>
|
||||||
|
{error && <p style={{ color: "crimson" }}>❌ {error}</p>}
|
||||||
|
{!error && !data && <p>Connecting…</p>}
|
||||||
|
{data && (
|
||||||
|
<>
|
||||||
|
<p>✅ <strong>{data.message}</strong></p>
|
||||||
|
<p style={{ color: "#666", fontSize: "0.85rem" }}>at {data.timestamp}</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
15
frontend/tsconfig.json
Normal file
15
frontend/tsconfig.json
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"types": ["vite/client"]
|
||||||
|
},
|
||||||
|
"include": ["src", "vite.config.ts"]
|
||||||
|
}
|
||||||
10
frontend/vite.config.ts
Normal file
10
frontend/vite.config.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
|
||||||
|
import viteReact from "@vitejs/plugin-react";
|
||||||
|
|
||||||
|
// TanStack Start full-stack app. The plugin wires SSR + file-based routing
|
||||||
|
// (routes live in src/routes, routeTree.gen.ts is generated automatically).
|
||||||
|
export default defineConfig({
|
||||||
|
server: { port: 3000, host: true },
|
||||||
|
plugins: [tanstackStart(), viteReact()],
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue