Uwagi praktyczne: Moja konfiguracja monorepo do wdrażania rozwiązań full-stack z pełną ochroną typów od początku do końca
Krok po kroku instrukcja obsługi Notatek praktycznych: moje ustawienie monorepo do wdrażania rozwiązań full-stack, end-to-end z bezpieczeństwem typów – umowy, sprawdzania oraz miejsca na kod do wklejenia dla zespołów stosujących ten wzorzec.
Niech to służy jako wersja przeznaczona dla operatorów, zawierająca ulepszone wersje pomysłów z artykułu „Moja konfiguracja monorepo do tworzenia aplikacji pełnoskalowych, end-to-end z bezpieczeństwem typów przy użyciu TypeScript, część 2”: wyraźne etapy, uporządkowane sekcje kodu oraz notatki naprawcze, które przetrwają przeniesienie obowiązków. Etap Przegląd działa najlepiej, gdy traktowany jest jako mierzalna powierzchnia do analizy. Zapisz jeden idealny przykład działania, jeden przypadek awarii oraz notatkę dotyczącą cofnięcia zmian, zanim rozszerzysz zakres pracy. Wolno preferować małe, testowalne jednostki zamiast rozbudowanych skryptów. Gdy jakiś krok zawiedzie, awaria powinna wskazywać na konkretną odpowiedzialność, a nie na skomplikowany łańcuch operacji.
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>
);
};
Lista kontrolna operacyjna
Gdy przechodzisz przez etap listy kontrolnej operacyjnej, najpierw zapisz umowę: wymagane dane wejściowe, sygnał sukcesu oraz to, co dzieje się w przypadku częściowego awarii. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie.
Zdokumentuj zarówno ścieżkę prawidłowego działania, jak i ścieżkę przywracania. Próby ponowne, mechanizmy ludzkiej kontroli oraz obsługa wiadomości błędowych stanowią część produktu, a nie elementy dodawane później.
Napisz krótki podręcznik obsługi: jak rotować klucze, jak opróżnić kolej z zadań, jak cofnąć ostatnie zadanie.
Zachowaj konfigurację poza kodem aplikacji. Pliki środowiskowe, magazyny tajnych danych oraz flagi funkcjonalne powinny znajdować się w jednym miejscu, które operatorzy mogą sprawdzić bez konieczności przeglądania całej struktury.
Gdy budżet na to pozwala, dodaj test dymny, który symuluje krytyczną ścieżkę w procesie CI przy użyciu prostych narzędzi, a nie rzeczywistych płatnych API.
Zapisuj czas trwania oraz koszt tokena lub zapytania obok wyników funkcjonalnych. Wczesna widoczność kosztów zapobiega niespodziewanym rachunkom, gdy ścieżka przechodzi z wersji demonstracyjnej do środowisk udostępnionych.
Zanim przesuniesz całą architekturę, zamroź wersje, utwórz dokładny zapis dla kluczowej ścieżki oraz potwierdź kroki odwracania zmian. Środowiska udostępnione wymagają ograniczeń szybkości, weryfikacji użytkowników oraz wyraźnego właściciela odpowiedzialnego za rotację haseł. Wolisz nudną niezawodność od sprytnych, jednorazowych demonstracji.
Uwaga dotycząca wersji 401e3ffdff5d: unikaj przechowywania kluczy dostawcy w repozytorium, ustaw ograniczenie liczby tokenów na sesję oraz przechowuj zapisy obok narzędzi do oceny, aby późniejsze zmiany modeli pozostały porównywalne.
Notatka dotycząca wzmocnienia bezpieczeństwa na etapie 0 działa najlepiej, gdy traktuje się ją jako mierzalną powierzchnię do analizy. Zapisz jeden idealny przypadek działania, jeden przypadek awarii oraz notatkę dotyczącą cofnięcia zmian, zanim rozszerzysz zakres prac. Zapisuj czasy wykonywania operacji oraz koszt tokenów lub zapytań obok wyników funkcjonalnych. Wczesna widoczność kosztów zapobiega niespodziewanym rachunkom, gdy proces przechodzi z środowiska demonstracyjnego do współdzielonych środowisk.
Szczegóły wzmocnienia bezpieczeństwa 0/962: zmierz czas wykonywania operacji, klasę błędu oraz zużycie tokenów dla tej notatki, a następnie zdecyduj, czy zachować zmianę na podstawie ustalonego zestawu pytań, a nie jedynie informacji anegdotycznych.
Dla etapu 1 notatki dotyczącej wzmocnienia bezpieczeństwa zdefiniuj dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed zmianą kodu. Operatorzy powinni móc ponownie wykonać ten krok na podstawie znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu systemu. Zdokumentuj zarówno prawidłowy przebieg procesu, jak i ścieżkę naprawczą. Próby ponownych działań, mechanizmy kontroli ludzkiej oraz obsługa wiadomości błędnych stanowią część produktu, a nie elementy dodawane później.
Szczegół wzmocnienia 1/962: zmierz czas wykonywania, klasę błędu oraz zużycie tokenów dla tej notatki, a następnie zdecyduj, czy zachować zmianę na podstawie ustalonego zestawu pytań, a nie jedynie anegdoty.
Podczas przechodzenia przez drugi etap notatki dotyczącej wzmocnienia, najpierw zapisz umowę: wymagane dane wejściowe, sygnał sukcesu oraz to, co dzieje się w przypadku częściowego niepowodzenia. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Traktuj ten etap jako umowę pomiędzy danymi wejściowymi a zweryfikowanymi wynikami. Nadaj nazwy poszczególnym elementom, zdefiniuj kryteria sukcesu i odrzuć ciche, częściowe ukończenie zadania.
Szczegół wzmocnienia 2/962: zmierz czas wykonywania, klasę błędu oraz zużycie tokenów dla tej notatki, a następnie zdecyduj, czy zachować zmianę na podstawie ustalonego zestawu pytań, a nie jedynie anegdoty.
Metoda zaostrzania bezpieczeństwa na etapie 3 działa najlepiej, gdy traktuje się ją jako mierzalną powierzchnię do analizy. Zapisz jeden idealny przykład działania, jeden przypadek awarii oraz notatkę dotyczącą cofnięcia zmian, zanim rozszerzysz zakres prac.
Szczegół zaostrzania bezpieczeństwa 3/962: zmierz czas wykonywania operacji, klasę błędu oraz zużycie tokenów dla tej notatki, a następnie zdecyduj, czy zachować zmianę, opierając się na ustalonej serii kryteriów, a nie na indywidualnych obserwacjach.
Dla etapu 4 notatki dotyczącej wzmacniania bezpieczeństwa należy zdefiniować dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed modyfikacją kodu. Operatorzy powinni móc ponownie wykonać dany krok na podstawie znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Należy preferować małe, testowalne jednostki nad rozbudowanymi skryptami. Gdy dany krok zawiedzie, powinien wskazywać na konkretną odpowiedzialność, a nie na skomplikowany proces.
Szczegół 4/962 dotyczący wzmacniania bezpieczeństwa: zmierz czas wykonywania, klasę błędu oraz zużycie tokenów dla tej notatki, a następnie zdecyduj, czy zachować zmianę, opierając się na ustalonej serii pytań, a nie na indywidualnych obserwacjach.
Literatura pokrewna
- Praktyczne notatki: Stek rozwojowca Agentic, warstwa po warstwie — Szczegółowy przewodnik po Praktycznych notatkach: Stek rozwojowca Agentic, warstwa po warstwie: kontrakty, sprawdzenia oraz miejsca na kod do wstawienia dla zespołów wdrażających ten wzorzec.
- Praktyczne notatki: TypeScript 7 – właśnie wydano 10 razy szybszy kompilator, oto co — Szczegółowy przewodnik po Praktycznych notatkach: TypeScript 7 – właśnie wydano 10 razy szybszy kompilator, oto co: kontrakty, sprawdzenia oraz miejsca na kod do wstawienia dla zespołów wdrażających ten wzorzec.