feat: scaffold FastAPI backend and Vue 3 frontend
Backend (backend/):
- FastAPI app with layered app/{api,core,crud,db,models,schemas} structure
- TiDB connection via SQLAlchemy. The URL is built with URL.create rather
than string formatting: the password contains '@', which would otherwise be
parsed as the user/host separator and silently truncate the credential.
- Alembic environment wired to Base.metadata and the app settings, so
backend/.env stays the single source of truth for credentials
- /api/health endpoint reporting database reachability
- models/ and crud/ are intentionally empty: no model classes are defined and
no tables are created, at import time or otherwise
Frontend (frontend/):
- Vue 3 + Vite + TypeScript scaffold (create-vue, --bare)
- axios instance with a normalized error shape
- vue-router with a home route and a catch-all 404
- pinia + pinia-plugin-persistedstate; the app store persists selected keys
- Element Plus with its icons registered globally
- dev proxy forwards /api to the FastAPI service on port 8000
The database paper_doc was created in TiDB out of band; no table exists yet.
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
import axios, { AxiosError, type AxiosInstance } from 'axios'
|
||||
|
||||
/**
|
||||
* Shared axios instance.
|
||||
*
|
||||
* `baseURL` defaults to the same-origin `/api` prefix, which Vite proxies to
|
||||
* the FastAPI service in development (see vite.config.ts). Point
|
||||
* `VITE_API_BASE_URL` at an absolute URL to bypass the proxy.
|
||||
*/
|
||||
export const http: AxiosInstance = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL ?? '/api',
|
||||
timeout: 15000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
/** Normalized failure shape, so views never have to inspect AxiosError. */
|
||||
export interface ApiError {
|
||||
message: string
|
||||
status?: number
|
||||
detail?: unknown
|
||||
}
|
||||
|
||||
function normalizeError(error: AxiosError): ApiError {
|
||||
const status = error.response?.status
|
||||
const payload = error.response?.data as { detail?: unknown } | undefined
|
||||
|
||||
let message: string
|
||||
if (payload?.detail && typeof payload.detail === 'string') {
|
||||
message = payload.detail
|
||||
} else if (error.code === 'ECONNABORTED') {
|
||||
message = 'The request timed out.'
|
||||
} else if (!error.response) {
|
||||
message = 'Could not reach the API. Is the backend running on port 8000?'
|
||||
} else {
|
||||
message = error.message
|
||||
}
|
||||
|
||||
return { message, status, detail: payload?.detail }
|
||||
}
|
||||
|
||||
http.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error: AxiosError) => Promise.reject(normalizeError(error)),
|
||||
)
|
||||
|
||||
export default http
|
||||
@@ -0,0 +1,15 @@
|
||||
import http from './client'
|
||||
|
||||
/** Mirrors `app.schemas.health.HealthResponse` on the backend. */
|
||||
export interface HealthResponse {
|
||||
status: string
|
||||
app: string
|
||||
database: string
|
||||
database_target: string
|
||||
}
|
||||
|
||||
/** Probe the backend, including its TiDB connection. */
|
||||
export async function fetchHealth(): Promise<HealthResponse> {
|
||||
const { data } = await http.get<HealthResponse>('/health')
|
||||
return data
|
||||
}
|
||||
Reference in New Issue
Block a user