Startseite / Artikel / Praktische Hinweise: Meine Monorepo-Einrichtung zur Bereitstellung vollständiger, end-to-end typsicherer Anwendungen

Praktische Hinweise: Meine Monorepo-Einrichtung zur Bereitstellung vollständiger, end-to-end typsicherer Anwendungen

Schritt-für-Schritt-Anleitung zu den Praktischen Hinweisen: Meine Monorepo-Einrichtung zur Bereitstellung vollständiger, end-to-end typsicherer Lösungen – mit Verträgen, Überprüfungen sowie Code-Blöcken für Teams, die dieses Muster nutzen.

2920 Wörter

Nutzen Sie dies als für Operator zugängliche Neuformulierung der Ideen aus „Meine Monorepo-Einrichtung zur Entwicklung vollständiger, end-to-end typsicherer Anwendungen mit TypeScript, Teil 2“: klare Phasen, geordnete Codeabschnitte sowie Wiederherstellungshinweise, die auch bei Übergaben erhalten bleiben. Die Überblicksphase funktioniert am besten, wenn sie als messbarer Rahmen betrachtet wird. Erfassen Sie vor der Erweiterung des Umfangs ein „goldenes“ Transkript, einen Fehlerfall sowie die Notizen zur Rücksetzung. Ziehen Sie kleine, testbare Einheiten vor großen Skripten vor. Wenn ein Schritt fehlschlägt, sollte der Fehler auf eine einzige Verantwortung verweisen und nicht auf einen verworrenen Ablaufprozess.

name: monorepo-setup

services:
  db:
    image: postgres:17-alpine
    container_name: monorepo-setup-database-container
    ports:
      - 127.0.0.1:5432:5432
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -d ${POSTGRES_DB} -U ${POSTGRES_USER}"]
      interval: 10s
      timeout: 5s
      retries: 5
    volumes:
      - monorepo-setup:/var/lib/postgresql/data
    env_file:
      - apps/api/.env.database

volumes:
  monorepo-setup:
    name: monorepo-setup
# apps/api/.env.database
POSTGRES_DB=monorepo-setup
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
docker compose up -d --remove-orphans
docker ps
docker exec -it monorepo-setup-database-container psql -U postgres -d monorepo-setup
# monorepo-setup-database-container is the name of the container, as specified in the compose.yaml file
# -U postgres (POSTGRES_USER specified in the .env.database file)
# same for -d monorepo-setup (POSTGRES_DB)
# From the root of the monorepo
cd apps/api
npm i drizzle-orm@rc pg dotenv
npm i -D drizzle-kit@rc tsx @types/pg
DATABASE_URL=postgres://<user>:<password>@<host>:<port>/<database>
DATABASE_URL=postgres://postgres:postgres@localhost:5432/monorepo-setup
nest g res drizzle
// apps/api/src/drizzle/constants.ts
export const DRIZZLE_ASYNC_PROVIDER = "DRIZZLE_ASYNC_PROVIDER";
// apps/api/drizzle/providers/drizzle.provider.ts

import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";

import { EnvService } from "@/env/env.service";

import { DRIZZLE_ASYNC_PROVIDER } from "../constants";

export const drizzleProvider = {
  provide: DRIZZLE_ASYNC_PROVIDER,
  inject: [EnvService],
  useFactory: async (envService: EnvService) => {
    const connectionString = envService.get("DATABASE_URL");
    const pool = new Pool({
      connectionString,
    });

    return drizzle({ client: pool });
  },
};
// apps/api/src/drizzle/schema/users.ts

import { pgTable, text, varchar } from "drizzle-orm/pg-core";
import { timestamp } from "drizzle-orm/pg-core";
import { ulid } from "ulid";

export const users = pgTable("users", {
  id: varchar("id", { length: 26 })
    .primaryKey()
    .notNull()
    .$defaultFn(() => ulid().toLowerCase()),

  username: text("username").notNull().unique(),
  email: text("email").notNull().unique(),
  hashedPassword: text("hashed_password").notNull(),

  createdAt: timestamp("created_at").defaultNow().notNull(),
  updatedAt: timestamp("updated_at")
    .defaultNow()
    .$onUpdate(() => new Date())
    .notNull(),
});
// apps/api/src/drizzle/schema/posts.ts

import { pgTable, text, varchar } from "drizzle-orm/pg-core";
import { timestamp } from "drizzle-orm/pg-core";
import { ulid } from "ulid";

import { users } from "./users";

export const posts = pgTable("posts", {
  id: varchar("id", { length: 26 })
    .primaryKey()
    .notNull()
    .$defaultFn(() => ulid().toLowerCase()),
  authorId: varchar("author_id", { length: 26 })
    .notNull()
    .references(() => users.id, { onDelete: "cascade" }),

  title: text("title").notNull(),
  slug: text("slug").notNull(),
  content: text("content").notNull(),

  publishedAt: timestamp("published_at"),
  createdAt: timestamp("created_at").defaultNow().notNull(),
  updatedAt: timestamp("updated_at")
    .defaultNow()
    .$onUpdate(() => new Date())
    .notNull(),
});
// apps/api/src/drizzle/schema/index.ts

export * from "./posts";
export * from "./users";
npm i ulid
// apps/api/src/env.ts

import { z } from "zod";

export const envSchema = z.object({
  NODE_ENV: z.enum(["production", "development"]),

  JWT_SECRET: z.string().trim().min(1),

  DASHBOARD_URL: z.url(),
  WEB_APP_URL: z.url(),

  DATABASE_URL: z.string().trim().min(1),
});

export type Env = z.infer<typeof envSchema>;
// apps/api/drizzle.config.ts

import { defineConfig } from "drizzle-kit";

import "dotenv/config";

export default defineConfig({
  out: "./src/drizzle/migrations",
  schema: "./src/drizzle/schema/index.ts",
  dialect: "postgresql",
  dbCredentials: {
    url: process.env.DATABASE_URL!,
  },
});
// apps/api/package.json

{
  "scripts": {
    ...
    "generate": "npx drizzle-kit generate",
    "migrate": "npx drizzle-kit migrate",
    ...
  }
}
npm run generate
npm run generate
docker exec -it monorepo-setup-database-container psql -U postgres -d monorepo-setup
\dt
// apps/api/package.json

{
  "scripts": {
    ...
   "studio": "npx drizzle-kit studio"
    ...
  }
}
npm run studio
import { EnvModule } from "@/env/env.module";
import { Module } from "@nestjs/common";

import { DRIZZLE_ASYNC_PROVIDER } from "./constants";
import { drizzleProvider } from "./providers/drizzle.provider";

@Module({
  imports: [EnvModule],
  providers: [drizzleProvider],
  exports: [DRIZZLE_ASYNC_PROVIDER],
})
export class DrizzleModule {}
import { Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";

import { EnvService } from "./env.service";

@Module({
  imports: [ConfigModule],
  providers: [EnvService],
  exports: [EnvService],
})
export class EnvModule {}
nest g res users
// apps/api/src/users/users.service.ts

import * as bcrypt from "bcrypt";
import { eq, or } from "drizzle-orm";
import { NodePgDatabase } from "drizzle-orm/node-postgres";

import { DRIZZLE_ASYNC_PROVIDER } from "@/drizzle/constants";
import { users } from "@/drizzle/schema";
import { Inject, Injectable } from "@nestjs/common";
import { TRPCError } from "@trpc/server";

import { CreateUserDtoType, FindByEmailDtoType } from "./users.dto";

@Injectable()
export class UsersService {
  private readonly saltRounds = 10;

  constructor(
    @Inject(DRIZZLE_ASYNC_PROVIDER)
    private readonly db: NodePgDatabase,
  ) {}

  async create(createUserDto: CreateUserDtoType) {
    const { email, password, username } = createUserDto;

    const [existingUserWithSameEmailOrUsername] = await this.db
      .select({ email: users.email, username: users.username })
      .from(users)
      .where(or(eq(users.email, email), eq(users.username, username)))
      .limit(1);

    if (existingUserWithSameEmailOrUsername) {
      if (existingUserWithSameEmailOrUsername.email === email) {
        throw new TRPCError({
          code: "CONFLICT",
          message: "This email is already used",
        });
      } else {
        throw new TRPCError({
          code: "CONFLICT",
          message: "This username already exists",
        });
      }
    }

    const hashedPassword = await bcrypt.hash(password, this.saltRounds);

    const [createdUser] = await this.db
      .insert(users)
      .values({
        username,
        email,
        hashedPassword,
      })
      .returning({
        id: users.id,
        email: users.email,
        username: users.username,
      });

    return createdUser;
  }

  async findByEmail(findByEmailDto: FindByEmailDtoType) {
    const { email } = findByEmailDto;

    const [user] = await this.db
      .select({
        id: users.id,
        email: users.email,
        username: users.username,
        hashedPassword: users.hashedPassword,
      })
      .from(users)
      .where(eq(users.email, email))
      .limit(1);

    if (!user) {
      return null;
    }

    return user;
  }
}
// apps/api/src/users/users.dto.ts

import z from "zod";

export const CreateUserDto = z.object({
  email: z.email(),
  username: z.string().min(1).max(25),
  password: z.string().min(1).max(72),
});

export const FindByEmailDto = z.object({
  email: z.email(),
});

export type CreateUserDtoType = z.infer<typeof CreateUserDto>;
export type FindByEmailDtoType = z.infer<typeof FindByEmailDto>;
# apps/api

npm i bcrypt
npm i -D @types/bcrypt
// apps/api/src/users/users.module.ts

import { DrizzleModule } from "@/drizzle/drizzle.module";
import { Module } from "@nestjs/common";

import { UsersService } from "./users.service";

@Module({
  imports: [DrizzleModule],
  providers: [UsersService],
  exports: [UsersService],
})
export class UsersModule {}
nest g res posts
import { and, eq } from "drizzle-orm";
import { NodePgDatabase } from "drizzle-orm/node-postgres";

import { DRIZZLE_ASYNC_PROVIDER } from "@/drizzle/constants";
import { posts } from "@/drizzle/schema";
import { Inject, Injectable } from "@nestjs/common";
import { TRPCError } from "@trpc/server";

import {
  CreatePostDtoType,
  FindAllPostsDtoType,
  FindOnePostDtoType,
} from "./posts.dto";

@Injectable()
export class PostsService {
  constructor(
    @Inject(DRIZZLE_ASYNC_PROVIDER)
    private readonly db: NodePgDatabase,
  ) {}
  async create(createPostDto: CreatePostDtoType) {
    const { title, slug, content, authorId } = createPostDto;

    const [existingPostWithSameSlug] = await this.db
      .select({ slug: posts.slug })
      .from(posts)
      .where(and(eq(posts.authorId, authorId), eq(posts.slug, slug)))
      .limit(1);

    if (existingPostWithSameSlug) {
      throw new TRPCError({
        code: "CONFLICT",
        message: "A post with that slug already exists",
      });
    }

    const [createdPost] = await this.db
      .insert(posts)
      .values({ title, slug, content, authorId, publishedAt: new Date() })
      .returning({
        id: posts.id,
        title: posts.title,
        slug: posts.slug,
        content: posts.content,
        authorId: posts.authorId,
      });

    return createdPost;
  }

  async findAll(findAllPostsDto: FindAllPostsDtoType) {
    const { authorId } = findAllPostsDto;

    const postsFetched = await this.db
      .select({ title: posts.title, slug: posts.slug, content: posts.content })
      .from(posts)
      .where(eq(posts.authorId, authorId));

    return postsFetched;
  }

  async findOne(findOnePostDto: FindOnePostDtoType) {
    const { authorId, slug } = findOnePostDto;

    const [post] = await this.db
      .select({
        title: posts.title,
        slug: posts.slug,
        content: posts.content,
      })
      .from(posts)
      .where(and(eq(posts.authorId, authorId), eq(posts.slug, slug)))
      .limit(1);

    if (!post) {
      return null;
    }

    return post;
  }
}
// apps/api/src/posts/posts.dto.ts

import z from "zod";

export const CreatePostDto = z.object({
  title: z.string().min(1).max(150),
  slug: z.string().min(1).max(150),
  content: z.string().min(1),
});

export const FindOnePostDto = z.object({
  slug: z.string().min(1).max(150),
});

type AuthorId = { authorId: string };
export type CreatePostDtoType = z.infer<typeof CreatePostDto> & AuthorId;
export type FindAllPostsDtoType = AuthorId;
export type FindOnePostDtoType = z.infer<typeof FindOnePostDto> & AuthorId;
// apps/api/src/posts/posts.module.ts

import { DrizzleModule } from "@/drizzle/drizzle.module";
import { Module } from "@nestjs/common";

import { PostsService } from "./posts.service";

@Module({
  imports: [DrizzleModule],
  providers: [PostsService],
  exports: [PostsService],
})
export class PostsModule {}
nest g res auth
// apps/api/src/auth/auth.service.ts

import * as bcrypt from "bcrypt";

import { JWTDtoType } from "@/common/dto";
import { UsersService } from "@/users/users.service";
import { Injectable } from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { TRPCError } from "@trpc/server";

import { LoginDtoType, RegisterDtoType } from "./auth.dto";

@Injectable()
export class AuthService {
  constructor(
    private readonly jwtService: JwtService,
    private readonly usersService: UsersService,
  ) {}

  async register(registerDto: RegisterDtoType) {
    const { username, email, password } = registerDto;

    const createdUser = await this.usersService.create({
      email,
      username,
      password,
    });

    const payload: Pick<JWTDtoType, "sub"> = { sub: createdUser.id };
    const token = await this.jwtService.signAsync(payload);

    return { accessToken: token };
  }

  async login(loginDto: LoginDtoType) {
    const { email, password } = loginDto;

    const user = await this.usersService.findByEmail({ email });

    if (!user) {
      throw new TRPCError({
        code: "NOT_FOUND",
        message: "User not found",
      });
    }

    const isPasswordCorrect = await bcrypt.compare(
      password,
      user.hashedPassword,
    );
    if (!isPasswordCorrect) {
      throw new TRPCError({
        code: "UNAUTHORIZED",
        message: "Incorrect password",
      });
    }

    const payload: Pick<JWTDtoType, "sub"> = { sub: user.id };
    const token = await this.jwtService.signAsync(payload);

    return {
      accessToken: token,
    };
  }
}
// apps/api/src/auth/auth.dto.ts

import z from "zod";

export const RegisterDto = z.object({
  username: z.string().min(1).max(25),
  email: z.email(),
  password: z.string().min(1).max(72),
});

export const LoginDto = z.object({
  email: z.email(),
  password: z.string().min(1).max(72),
});

export type RegisterDtoType = z.infer<typeof RegisterDto>;
export type LoginDtoType = z.infer<typeof LoginDto>;
// apps/api/src/auth/auth.router.ts

import { TrpcService } from "@/trpc/trpc.service";
import { Injectable } from "@nestjs/common";

import { LoginDto, RegisterDto } from "./auth.dto";
import { AuthService } from "./auth.service";

@Injectable()
export class AuthRouter {
  constructor(
    private readonly trpcService: TrpcService,
    private readonly authService: AuthService,
  ) {}

  private readonly AUTH_COOKIE_NAME = "auth_token";
  private readonly COOKIE_MAX_AGE = 2 * 24 * 60 * 60 * 1000; // 2 days

  procedures() {
    return {
      auth: this.trpcService.trpc.router({
        register: this.trpcService
          .publicProcedure()
          .input(RegisterDto)
          .mutation(async ({ ctx, input }) => {
            const { accessToken } = await this.authService.register(input);

            // Set the HTTP-only cookie
            ctx.res.cookie(this.AUTH_COOKIE_NAME, accessToken, {
              httpOnly: true,
              secure: true,
              sameSite: "lax",
              maxAge: this.COOKIE_MAX_AGE,
            });
          }),

        login: this.trpcService
          .publicProcedure()
          .input(LoginDto)
          .mutation(async ({ ctx, input }) => {
            const { accessToken } = await this.authService.login(input);

            // Set the HTTP-only cookie
            ctx.res.cookie(this.AUTH_COOKIE_NAME, accessToken, {
              httpOnly: true,
              secure: true,
              sameSite: "lax",
              maxAge: this.COOKIE_MAX_AGE,
            });
          }),
      }),
    };
  }
}
// apps/api/src/auth/auth.module.ts

import { EnvModule } from "@/env/env.module";
import { EnvService } from "@/env/env.service";
import { TrpcService } from "@/trpc/trpc.service";
import { UsersModule } from "@/users/users.module";
import { Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { JwtModule } from "@nestjs/jwt";

import { AuthRouter } from "./auth.router";
import { AuthService } from "./auth.service";

@Module({
  imports: [
    ConfigModule,
    UsersModule,
    JwtModule.registerAsync({
      imports: [EnvModule],
      useFactory: async (envService: EnvService) => ({
        global: true,
        secret: envService.get("JWT_SECRET"),
        signOptions: { expiresIn: "2d" },
      }),
      inject: [EnvService],
    }),
  ],

  providers: [AuthService, AuthRouter, TrpcService, EnvService],
  exports: [AuthRouter, AuthService],
})
export class AuthModule {}
// apps/api/src/trpc/trpc.router.ts

  appRouter = this.trpcService.trpc.router({
    getHello: this.trpcService
      .publicProcedure()
      .input(
        z.object({
          name: z.string().min(1),
        }),
      )
      .query(({ input }) => `Hello, ${input.name} from trpc server`),

    ...this.authRouter.procedures(),
  });
// apps/api/src/trpc/trpc.module.ts

@Module({
  imports: [AuthModule],
  providers: [TrpcService, TrpcRouter, JwtService, EnvService, AuthRouter],
  exports: [TrpcService],
})
// apps/api/src/posts/posts.router.ts

import { TrpcService } from "@/trpc/trpc.service";
import { Injectable } from "@nestjs/common";

import { CreatePostDto, FindOnePostDto } from "./posts.dto";
import { PostsService } from "./posts.service";

@Injectable()
export class PostsRouter {
  constructor(
    private readonly trpcService: TrpcService,
    private readonly postsService: PostsService,
  ) {}

  procedures() {
    return {
      posts: this.trpcService.trpc.router({
        create: this.trpcService
          .protectedProcedure()
          .input(CreatePostDto)
          .mutation(async ({ ctx, input }) =>
            this.postsService.create({ ...input, authorId: ctx.user.sub }),
          ),

        findAll: this.trpcService
          .protectedProcedure()
          .query(async ({ ctx }) =>
            this.postsService.findAll({ authorId: ctx.user.sub }),
          ),

        findOne: this.trpcService
          .protectedProcedure()
          .input(FindOnePostDto)
          .query(async ({ ctx, input }) =>
            this.postsService.findOne({ ...input, authorId: ctx.user.sub }),
          ),
      }),
    };
  }
}
// apps/api/src/trpc/trpc.router.ts

  appRouter = this.trpcService.trpc.router({
    getHello: this.trpcService
      .publicProcedure()
      .input(
        z.object({
          name: z.string().min(1),
        }),
      )
      .query(({ input }) => `Hello, ${input.name} from trpc server`),

    ...this.authRouter.procedures(),
    ...this.postsRouter.procedures(),
  });
// apps/api/src/trpc/trpc.module.ts

@Module({
  imports: [AuthModule, PostsModule],
  providers: [
    TrpcService,
    TrpcRouter,
    JwtService,
    EnvService,
    AuthRouter,
    PostsRouter,
  ],
  exports: [TrpcService],
})
import { Controller, useForm } from "react-hook-form";
import { Link, useNavigate } from "react-router";
import { toast } from "sonner";

import { useTRPC } from "@/utils/trpc";
import { zodResolver } from "@hookform/resolvers/zod";
import { LoginSchema, type LoginSchemaType } from "@repo/common/types-schemas";
import { Button } from "@repo/ui/components/ui/button";
import { Field, FieldError, FieldLabel } from "@repo/ui/components/ui/field";
import { Input } from "@repo/ui/components/ui/input";
import { useMutation } from "@tanstack/react-query";

import { useTogglePassword } from "../hooks/use-toggle-password";

export const LoginForm = () => {
  const form = useForm<LoginSchemaType>({
    resolver: zodResolver(LoginSchema),
    defaultValues: {
      email: "",
      password: "",
    },
  });

  const { isPasswordVisible, EyeIconComponent } = useTogglePassword();

  const navigate = useNavigate();
  const trpc = useTRPC();
  const loginMutation = useMutation(trpc.auth.login.mutationOptions());

  const onSubmit = (values: LoginSchemaType) => {
    loginMutation.mutate(
      {
        email: values.email,
        password: values.password,
      },
      {
        onError: (error) => {
          const errorMessage = error.message;

          if (errorMessage.toLowerCase().includes("email")) {
            form.setError("email", { message: errorMessage });
          } else if (errorMessage.toLowerCase().includes("password")) {
            form.setError("password", { message: errorMessage });
          } else {
            form.setError("root", { message: errorMessage });
          }
        },

        onSuccess: () => {
          toast.success("Login complete");

          navigate("/posts");
        },
      },
    );
  };

  return (
    <form
      onSubmit={form.handleSubmit(onSubmit)}
      className="flex w-[clamp(40%,30rem,80%)] flex-col gap-4 p-2"
    >
      <h2 className="flex flex-col items-center justify-center gap-2 text-center text-3xl font-extrabold max-[42.5rem]:text-2xl">
        Login
      </h2>

      <Controller
        control={form.control}
        name="email"
        render={({ field, fieldState }) => (
          <Field data-invalid={fieldState.invalid}>
            <FieldLabel htmlFor={field.name}>Email</FieldLabel>
            <Input
              id={field.name}
              placeholder="example@email.com"
              type="email"
              aria-invalid={fieldState.invalid}
              className="border-border h-10"
              {...field}
            />
            {fieldState.invalid && <FieldError errors={[fieldState.error]} />}
          </Field>
        )}
      />

      <Controller
        control={form.control}
        name="password"
        render={({ field, fieldState }) => (
          <Field data-invalid={fieldState.invalid}>
            <FieldLabel htmlFor={field.name}>Password</FieldLabel>
            <div className="relative flex items-center justify-end">
              <Input
                id={field.name}
                placeholder="***********"
                type={isPasswordVisible ? "text" : "password"}
                aria-invalid={fieldState.invalid}
                className="border-border h-10"
                {...field}
              />
              <EyeIconComponent />
            </div>
            {fieldState.invalid && <FieldError errors={[fieldState.error]} />}
          </Field>
        )}
      />
      <p>
        Not registered yet?{" "}
        <Link to="/register" className="underline">
          Register
        </Link>
      </p>

      <Button
        variant="default"
        type="submit"
        disabled={form.formState.isSubmitting}
        className="h-10 w-1/2 self-center rounded-lg"
      >
        Submit
      </Button>

      <div className="h-4">
        {form.formState.errors.root && (
          <FieldError errors={[form.formState.errors.root]} />
        )}
      </div>
    </form>
  );
};

Betriebskontrollliste

Während der Erstellung der Betriebskontrollliste sollten zunächst die Anforderungen des Vertrags aufgeschrieben werden: erforderliche Eingaben, Erfolgsindikatoren sowie Vorgehensweisen bei teilweisen Fehlern. Diese Kontrollliste sorgt dafür, dass spätere Codeänderungen transparent bleiben.

Dokumentieren Sie sowohl den erfolgreichen Ablauf als auch die Wiederherstellungsprozesse gemeinsam. Wiederholversuche, menschliche Überprüfungen sowie die Handhabung von Fehlern gehören zum Produkt selbst und nicht zu späteren Optimierungen.

Schreiben Sie ein kurzes Handbuch: Wie werden Schlüssel ausgetauscht, wie wird die Warteschlange geleert und wie wird der letzte Eingang rückgängig gemacht?

Lassen Sie die Konfiguration außerhalb des Anwendungscode liegen. Umgebungsdateien, Geheimdatenspeicher sowie Feature-Flags sollten an einem Ort gesammelt sein, den Betreuer ohne das Durchlesen des gesamten Systems überprüfen können.

Fügen Sie sofern das Budget es zulässt in den CI-Prozess einen Smoke-Test hinzu, der den kritischen Ablauf mit Fixtures und nicht mit live genutzten, bezahlten APIs testet.

Erhalten Sie Aufzeichnungen der Laufzeiten sowie der Kosten für Token oder Abfragen neben den funktionalen Ergebnissen. Eine frühzeitige Sichtbarkeit der Kosten verhindert überraschende Rechnungen, wenn sich der Pfad von einer Demo in gemeinsame Umgebungen verschiebt.

Vor der Einführung des Stack sollten Sie Versionen einfrieren, ein „goldenes“ Transkript für den kritischen Pfad erstellen und die Schritte zum Rollback bestätigen. Gemeinsame Umgebungen erfordern Rate Limits, Überprüfungen der Nutzungsrechte sowie einen klaren Verantwortlichen für die Rotation von Geheimnissen. Wählen Sie langweilige Zuverlässigkeit statt cleverer, einmaliger Demos.

Batch-Hinweis für 401e3ffdff5d: Halten Sie die Schlüssel des Anbieters außerhalb des Repositories, legen Sie eine Obergrenze für Token pro Sitzung fest und speichern Sie die Transkripte neben den Evaluierungs-Dateien, damit spätere Modellwechsel vergleichbar bleiben.

Die Verstärkungsanweisung Stufe 0 funktioniert am besten, wenn sie als messbare Oberfläche betrachtet wird. Erfassen Sie vor Erweiterung des Umfangs ein „goldenes“ Transkript, einen Fehlerfall sowie die Rollback-Anweisung. Notieren Sie außerdem die Laufzeiten sowie die Kosten für Tokens oder Abfragen neben den funktionalen Ergebnissen. Eine frühzeitige Sichtbarkeit der Kosten verhindert überraschende Rechnungen, wenn der Prozess von einer Demo-Umgebung in gemeinsam genutzte Umgebungen übergeht.

Verstärkungsdetail 0/962: Messen Sie für diese Anweisung die Gesamtlaufzeit, die Fehlerklasse sowie den Tokenverbrauch und entscheiden Sie anschließend auf der Grundlage eines festgelegten Fragebogens – und nicht nur aufgrund von Einzelfällen –, ob die Änderung beibehalten werden soll.

Für die Verstärkungsanweisung Stufe 1 sollten Sie vor dem Ändern des Codes die Eingabedaten, den Verantwortlichen für den jeweiligen Schritt sowie die Abbruchkriterien definieren. Die Operator sollten in der Lage sein, den Schritt von einem bekannten Checkpoint aus erneut auszuführen, ohne auf versteckte Zustände schließen zu müssen. Dokumentieren Sie sowohl den erfolgreichen Ablauf als auch den Notfallweg gemeinsam. Wiederholungsversuche, menschliche Kontrollen sowie die Handhabung von Fehlern gehören zum Produkt selbst und nicht zu späteren Optimierungen.

Verstärkungsmaßnahme Detail 1/962: Messen Sie die Ausführungsdauer, die Fehlerklasse sowie den Tokenverbrauch für diese Notiz und entscheiden Sie anschließend auf der Grundlage eines festgelegten Fragebogens statt aufgrund von Einzelfällen, ob die Änderung beibehalten werden soll.

Beim Bearbeiten der zweiten Phase der Verstärkungsmaßnahme sollten Sie zunächst den Vertrag aufschreiben: erforderliche Eingaben, Erfolgsindikator sowie das Vorgehen bei teilweisen Fehlern. Diese Checkliste sorgt dafür, dass spätere Codeänderungen transparent bleiben. Betrachten Sie diese Phase als Vertrag zwischen Eingaben und validierten Ausgaben. Benennen Sie die Artefakte, definieren Sie Erfolgsprüfungen und lehnen Sie stille, teilweise abgeschlossene Abläufe ab.

Verstärkungsmaßnahme Detail 2/962: Messen Sie die Ausführungsdauer, die Fehlerklasse sowie den Tokenverbrauch für diese Notiz und entscheiden Sie anschließend auf der Grundlage eines festgelegten Fragebogens statt aufgrund von Einzelfällen, ob die Änderung beibehalten werden soll.

Die Verstärkungsmaßnahme Stufe 3 funktioniert am besten, wenn sie als messbare Oberfläche betrachtet wird. Erfassen Sie ein „goldenes“ Transkript, einen Fehlerfall sowie die Rollback-Anmerkung, bevor Sie den Umfang erweitern. Bewahren Sie die Konfiguration außerhalb des Anwendungscode auf. Umgebungsdateien, Geheimdatenspeicher und Feature-Flags sollten an einem Ort gesammelt sein, den Betreiber ohne das Durchlesen des gesamten Systems überprüfen können.

Detail der Verstärkungsmaßnahme 3/962: Messen Sie für diese Anmerkung die Ausführungszeit, die Fehlerklasse sowie den Tokenverbrauch und entscheiden Sie anschließend auf der Grundlage eines festgelegten Fragebogens – statt aufgrund von Einzelfällen –, ob die Änderung beibehalten werden soll.

Zur Stufe 4 der Verstärkungsmaßnahmen sollten die Eingabedaten, der Verantwortliche für den Schritt sowie die Abbruchkriterien vor dem Codeändern definiert werden. Die Operator sollten in der Lage sein, den Schritt von einem bekannten Checkpoint aus erneut auszuführen, ohne auf versteckte Zustände schließen zu müssen. Es ist vorzuziehen, kleine, testbare Einheiten statt umfangreicher Skripte zu verwenden. Wenn ein Schritt fehlschlägt, sollte der Fehler auf eine einzige Verantwortung verweisen und nicht auf ein verworrenes Ablaufverfahren.

Detail zur Verstärkung 4/962: Messen Sie die Ausführungszeit, die Fehlerklasse sowie den Tokenverbrauch für diese Maßnahme und entscheiden Sie anschließend auf der Grundlage eines festgelegten Fragebogens statt von Einzelfallberichten, ob die Änderung beibehalten werden soll.