Wire frontend into Docker Compose with type-safe API client

- Add frontend service to docker-compose.yml (port 3000, BACKEND_URL env var)
- Add frontend/Dockerfile using adapter-node for plain Node/Docker runtime
- Switch svelte.config.js from adapter-auto to adapter-node
- Generate OpenAPI types from backend spec (openapi-typescript + openapi-fetch)
- Add src/lib/server/api.ts as server-only typed API client factory
- Add generate:api script to regenerate types when backend spec changes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-02 12:36:09 +02:00
parent b36d4c731d
commit 82815205d0
8 changed files with 6135 additions and 0 deletions

View File

@@ -32,5 +32,17 @@ services:
db:
condition: service_healthy
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
container_name: mealprep-frontend
ports:
- "3000:3000"
environment:
BACKEND_URL: http://app:8080
depends_on:
- app
volumes:
pgdata:

15
frontend/Dockerfile Normal file
View File

@@ -0,0 +1,15 @@
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:22-alpine
WORKDIR /app
COPY --from=build /app/build build/
COPY --from=build /app/node_modules node_modules/
COPY package.json .
EXPOSE 3000
ENV NODE_ENV=production
CMD ["node", "build"]

4031
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

41
frontend/package.json Normal file
View File

@@ -0,0 +1,41 @@
{
"name": "frontend",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"test": "vitest run",
"test:watch": "vitest",
"test:ui": "vitest --ui",
"test:e2e": "playwright test",
"generate:api": "curl -s http://localhost:8080/v3/api-docs -o src/lib/api/openapi.json && openapi-typescript src/lib/api/openapi.json -o src/lib/api/schema.d.ts"
},
"devDependencies": {
"@playwright/test": "^1.59.1",
"@sveltejs/adapter-auto": "^7.0.0",
"@sveltejs/adapter-node": "^5.5.4",
"@sveltejs/kit": "^2.50.2",
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"@tailwindcss/vite": "^4.2.2",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/svelte": "^5.3.1",
"@vitest/ui": "^4.1.2",
"jsdom": "^29.0.1",
"openapi-typescript": "^7.13.0",
"svelte": "^5.54.0",
"svelte-check": "^4.4.2",
"tailwindcss": "^4.2.2",
"typescript": "^5.9.3",
"vite": "^7.3.1",
"vitest": "^4.1.2"
},
"dependencies": {
"openapi-fetch": "^0.17.0"
}
}

File diff suppressed because one or more lines are too long

1992
frontend/src/lib/api/schema.d.ts vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,19 @@
import createClient from 'openapi-fetch';
import type { paths } from '$lib/api/schema.d.ts';
import { env } from '$env/dynamic/private';
// Usage in +page.server.ts load functions and form actions:
//
// export const load = async ({ fetch }) => {
// const api = apiClient(fetch);
// const { data, error } = await api.GET('/v1/recipes', { ... });
// };
//
// Always pass SvelteKit's `fetch` so session cookies are forwarded correctly.
export function apiClient(fetch?: typeof globalThis.fetch) {
return createClient<paths>({
baseUrl: env.BACKEND_URL ?? 'http://localhost:8080',
fetch: fetch ?? globalThis.fetch
});
}

24
frontend/svelte.config.js Normal file
View File

@@ -0,0 +1,24 @@
import adapter from '@sveltejs/adapter-node';
import { relative, sep } from 'node:path';
/** @type {import('@sveltejs/kit').Config} */
const config = {
compilerOptions: {
// defaults to rune mode for the project, except for `node_modules`. Can be removed in svelte 6.
runes: ({ filename }) => {
const relativePath = relative(import.meta.dirname, filename);
const pathSegments = relativePath.toLowerCase().split(sep);
const isExternalLibrary = pathSegments.includes('node_modules');
return isExternalLibrary ? undefined : true;
}
},
kit: {
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
adapter: adapter()
}
};
export default config;