feat: Grundgeruest fertigstellen (Backend/Frontend/K8s)

Backend: Prisma-Client auf prisma-client-js + Driver-Adapter (@prisma/adapter-pg)
umgestellt (Prisma 7 verlangt Adapter, ESM-Client brach an Jest), erste Migration
erzeugt und angewendet, /api/v1 Versioning, Swagger unter /api/v1/docs, Helmet,
globale ValidationPipe, PrismaModule, HealthModule (/health, /health/ready mit
DB-Check), tsconfig.build.json Root-Dir-Fix (dist/main.js statt dist/src/main.js).

Frontend: Tailwind v4 + shadcn/ui initialisiert, TanStack Query Provider, PWA
Manifest (app/manifest.ts), Vitest + Testing Library Setup, next.config output
standalone.

deploy/k8s: Dockerfiles (Backend/Frontend, Multi-Stage) + Deployment/Service/
Ingress/ConfigMap/Secret-Vorlagen fuer beide Projekte.

Lokale Dev-/Test-Postgres-Instanz unter ApiCore-Backend/.devdb angelegt (eigene
Rolle, kein bestehendes Passwort noetig), .env/.env.test lokal befuellt.

Lint, Typecheck, Tests und Build sind fuer Backend und Frontend gruen; Server
wurde lokal gestartet und /health, /health/ready, /api/v1/docs manuell verifiziert.
This commit is contained in:
Nick Szaunig committed 2026-08-21 05:17:26 +02:00
1 parent c4a2d0648e
commit eae3fea957
57 files changed
+15114 -35

No files matched your search

+8 -1
View File
@@ -1,9 +1,16 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { PrismaModule } from './prisma/prisma.module';
import { HealthModule } from './health/health.module';
@Module({
imports: [],
imports: [
ConfigModule.forRoot({ isGlobal: true }),
PrismaModule,
HealthModule,
],
controllers: [AppController],
providers: [AppService],
})
@@ -0,0 +1,30 @@
import { ServiceUnavailableException } from '@nestjs/common';
import { HealthController } from './health.controller';
import { PrismaService } from '../prisma/prisma.service';
describe('HealthController', () => {
let controller: HealthController;
let prisma: { $queryRaw: jest.Mock };
beforeEach(() => {
prisma = { $queryRaw: jest.fn() };
controller = new HealthController(prisma as unknown as PrismaService);
});
it('liveness returns ok without touching the database', () => {
expect(controller.liveness()).toEqual({ status: 'ok' });
expect(prisma.$queryRaw).not.toHaveBeenCalled();
});
it('readiness returns ok when the database responds', async () => {
prisma.$queryRaw.mockResolvedValueOnce([{ '?column?': 1 }]);
await expect(controller.readiness()).resolves.toEqual({ status: 'ok' });
});
it('readiness throws ServiceUnavailableException when the database is unreachable', async () => {
prisma.$queryRaw.mockRejectedValueOnce(new Error('connection refused'));
await expect(controller.readiness()).rejects.toBeInstanceOf(
ServiceUnavailableException,
);
});
});
@@ -0,0 +1,29 @@
import {
Controller,
Get,
ServiceUnavailableException,
VERSION_NEUTRAL,
} from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { PrismaService } from '../prisma/prisma.service';
@ApiTags('health')
@Controller({ path: 'health', version: VERSION_NEUTRAL })
export class HealthController {
constructor(private readonly prisma: PrismaService) {}
@Get()
liveness(): { status: 'ok' } {
return { status: 'ok' };
}
@Get('ready')
async readiness(): Promise<{ status: 'ok' }> {
try {
await this.prisma.$queryRaw`SELECT 1`;
return { status: 'ok' };
} catch {
throw new ServiceUnavailableException('Database not reachable');
}
}
}
@@ -0,0 +1,7 @@
import { Module } from '@nestjs/common';
import { HealthController } from './health.controller';
@Module({
controllers: [HealthController],
})
export class HealthModule {}
+32 -1
View File
@@ -1,8 +1,39 @@
import { NestFactory } from '@nestjs/core';
import { RequestMethod, ValidationPipe, VersioningType } from '@nestjs/common';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import helmet from 'helmet';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(helmet());
app.setGlobalPrefix('api', {
exclude: [
{ path: 'health', method: RequestMethod.GET },
{ path: 'health/ready', method: RequestMethod.GET },
],
});
app.enableVersioning({ type: VersioningType.URI, defaultVersion: '1' });
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
);
const swaggerConfig = new DocumentBuilder()
.setTitle('ApiCore API')
.setDescription('Digitale Imkereiverwaltung – API-Dokumentation')
.setVersion('1.0')
.addBearerAuth()
.build();
const swaggerDocument = SwaggerModule.createDocument(app, swaggerConfig);
SwaggerModule.setup('api/v1/docs', app, swaggerDocument);
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
void bootstrap();
@@ -0,0 +1,9 @@
import { Global, Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';
@Global()
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}
@@ -0,0 +1,21 @@
import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
import { PrismaPg } from '@prisma/adapter-pg';
import { PrismaClient } from '../../generated/prisma';
@Injectable()
export class PrismaService
extends PrismaClient
implements OnModuleInit, OnModuleDestroy
{
constructor() {
super({ adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }) });
}
async onModuleInit(): Promise<void> {
await this.$connect();
}
async onModuleDestroy(): Promise<void> {
await this.$disconnect();
}
}