IlmHamroh
JavaScript Full-stack/25-qism. HTTP, Express va REST API37/51-dars22 daqiqa
Mundarija (27)

Env validatsiyasi Zod bilan: tipli config obyekti va fail-fast

Qisqacha: Noto'g'ri sozlama bilan ilova umuman ishga tushmasligi kerak — xato tunda, birinchi so'rovda emas, deploy paytida ko'rinsin (fail-fast). Buning uchun process.env bitta joyda — configYukla(env) funksiyasida o'qiladi va Zod sxemasidan o'tadi: satrlar songa, ro'yxatga, true/false ga aylanadi, sukutlar qo'yiladi. Natija — tipli va muzlatilgan Config obyekti; qolgan kod faqat shuni oladi. Xato bo'lsa — hamma xatolar bitta ro'yxatda (z.prettifyError), qiymatlarsiz (ular sir bo'lishi mumkin), va exit 78.

Bu darsda

  • Fail-fast tamoyilini config'ga qo'llaysiz va exit 78 (EX_CONFIG) ma'nosini bilasiz.
  • Zod bilan env sxemasini yozasiz: coerce, enum, default, transform + pipe, refine.
  • z.output dan tipli Config olasiz va uni muzlatasiz.
  • process.env ni faqat kirish nuqtasida o'qib, qolgan kodni testlanadigan qilasiz.
  • Kanon topilmalari va biz topgan ikki teshikni bilasiz; envalid va @t3-oss/env-core bilan taqqoslaysiz.

Oldin bilishingiz kerak: Environment o'zgaruvchilar va .env, Zod 4 serverda, node-config, Xato strategiyasi.

1. Nega bu kerak?

env darsida PORT= bilan server jim tasodifiy portda ochilganini ko'rdik. node-config darsida PORT=yuz bilan port maydoni jim yo'qoldi, NODE_ENV=prod bilan production dev sozlamalarda ishladi. Uchala holatda umumiy narsa — jimlik: ilova ishga tushdi va "hammasi joyida" dedi.

Endi tasavvur qiling, bu deploy paytida bo'ldi. Eski versiya o'chirildi, yangisi "ishga tushdi" — health check ham yashil. Bir soatdan keyin frontend'dagilar yozadi: "CORS xatosi". Sabab — CORS_ORIGINS da bitta vergul tushib qolgan.

Agar ilova birinchi soniyada "CORS_ORIGINS noto'g'ri" deb o'lsa edi — deploy darhol muvaffaqiyatsiz bo'lardi va eski versiya ishlashda qolardi. Xato strategiyasi darsidagi fail-fast ("tez yiqilish") tamoyili config uchun eng foydali.

O'xshatish: uchuvchining parvoz oldi tekshiruv ro'yxati. Yoqilg'i, qanotlar, asboblar — hammasi uchishdan oldin tekshiriladi. Biror narsa noto'g'ri bo'lsa, samolyot uchmaydi. Havoda bilib qolish — eng qimmat yo'l.

2. Bitta joy: configYukla(env)

2.1 Qoida

Holat 34 da process.env uch joyda o'qilardi. Endi qoida:

Maslahat: process.env faqat bitta joyda o'qiladi — kirish nuqtasida (server.ts). U configYukla(process.env) ga beriladi. Qolgan kod Config obyektini parametr sifatida oladi.

Bu ikki narsa beradi. Birinchi: hamma sozlamalar ro'yxati — bitta faylda, bitta sxemada. Ikkinchi: test process.env ni o'zgartirmasdan, oddiy obyekt bilan ishlaydi — configYukla({ PORT: "4100" }). process.env global; uni testda o'zgartirish boshqa testlarga "sizib" o'tadi.

2.2 Ishga tushish tartibi

flowchart TD
  A[process.env] --> B["configYukla(env)"]
  B --> C{Zod sxemasi}
  C -- yaroqli --> D["Config<br/>tipli, muzlatilgan"]
  C -- xato --> E["xatolar ro'yxati<br/>qiymatlarsiz"]
  E --> F[exit 78]
  D --> G[omborYarat]
  D --> H[ishgaTushir]
  D --> I[jarayonniBoshqar]

Diagrammada Config dan keyingi hamma narsa — oddiy funksiya chaqiruvi. Ular env haqida hech narsa bilmaydi.

Xuddi shu tartib — qadamma-qadam, PORT=yuz bilan ishga tushirilganda:

Qadam Kod Natija
1 server.ts process.env ni oladi { PORT: "yuz", … } — hamma qiymat satr
2 configYukla(env) → EnvSxemasi.safeParse PORT songa aylanmadi — xato
3 ConfigXatosi tashlanadi xabarda kalit nomi bor, qiymat yo'q
4 server.ts uni ushlaydi console.error + exit 78
5 omborYarat, ishgaTushir chaqirilmaydi — port ham ochilmaydi

Beshinchi qator — fail-fast'ning o'zi: noto'g'ri sozlama bilan bitta so'rov ham qabul qilinmaydi.

3. Zod bilan env sxemasi

3.1 Birinchi sxema

Zod darsida so'rov tanasini tekshirgansiz. Env — boshqa manba, lekin vosita o'sha. Farqi: env'da hamma qiymat satr, shuning uchun aylantirish ko'p. Blok Node'da (o'z terminalingizda) ishlaydi — Zod 4 paket sifatida o'rnatilgan bo'lishi kerak:

ts
import { z } from "zod";

const EnvSxemasi = z.object({
  NODE_ENV: z.enum(["development", "production", "test"])
    .default("development"),
  PORT: z.coerce.number().int().min(0).max(65_535).default(3000),
  HOST: z.string().min(1).default("127.0.0.1"),
  SAQLASH: z.enum(["xotira", "fayl"]).default("xotira"),
});

const c = EnvSxemasi.parse({ PORT: "4100" });
console.log(c);
console.log(typeof c.PORT);

Konsolda:

text
{
  NODE_ENV: 'development',
  PORT: 4100,
  HOST: '127.0.0.1',
  SAQLASH: 'xotira'
}
number

Har qism:

Qism Nima qiladi
z.coerce.number() "4100" → 4100 (Number(x))
.int().min(0).max(65_535) Port chegarasi
z.enum([...]) Faqat ro'yxatdagi qiymat — prod xato beradi
.default(x) Kalit yo'q bo'lsa — sukut

65_535 — sondagi pastki chiziq faqat o'qish uchun, qiymati 65535.

3.2 Hamma xato birdaniga, qiymatsiz

Endi uchta xato bilan:

ts
import { z } from "zod";

const EnvSxemasi = z.object({
  PORT: z.coerce.number().int().min(0).max(65_535).default(3000),
  SAQLASH: z.enum(["xotira", "fayl"]).default("xotira"),
  WEBHOOK_URL: z.url().optional(),
});

const n = EnvSxemasi.safeParse({
  PORT: "yuz",
  SAQLASH: "mongo",
  WEBHOOK_URL: "parol-bor-manzil",
});
if (!n.success) console.log(z.prettifyError(n.error));

Konsolda:

text
✖ Invalid input: expected number, received NaN
  → at PORT
✖ Invalid option: expected one of "xotira"|"fayl"
  → at SAQLASH
✖ Invalid URL
  → at WEBHOOK_URL

Uch narsa muhim:

  • Hammasi birdaniga. parse birinchi xatoda to'xtamaydi. Dasturchi uchta xatoni bitta urinishda tuzatadi, uch marta deploy qilmaydi.
  • z.prettifyError — Zod 4 ning odam o'qiydigan formati: har xato ✖ bilan, → at — qaysi kalit.
  • Qiymatlar yo'q. "parol-bor-manzil" xabarda ko'rinmadi. WEBHOOK_URL ichida token bo'lishi mumkin (sirlar darsi) — xato logi esa CI'da hammaga ochiq.

3.3 Murakkab qiymatlar: transform + pipe

CORS_ORIGINS=http://localhost:5173,https://login.github.io — bitta satr, lekin ma'nosi URL'lar ro'yxati. Kanon sxemasi (holat 40):

ts
import { z } from "zod";

const Cors = z.string().default("")
  .transform((x) => x.split(",").map((o) => o.trim())
    .filter((o) => o !== ""))
  .pipe(z.array(z.url()));
const Rost = z.enum(["true", "false"])
  .transform((x) => x === "true");

console.log(Cors.parse(
  "http://localhost:5173, https://login.github.io"));
console.log(Cors.parse(undefined));
console.log(Rost.parse("false"), Rost.safeParse("TRUE").success);

Konsolda:

text
[ 'http://localhost:5173', 'https://login.github.io' ]
[]
false false

transform — satrni massivga aylantiradi; pipe — natijani yana sxemadan o'tkazadi (har element URL bo'lsin). Rost — env darsidagi "false" truthy tuzog'ining davosi: faqat aniq "true" yoki "false". "TRUE" — xato, taxmin qilinmaydi.

Tekshirib ko'ring: Nega HSTS: z.coerce.boolean() ishlatilmadi?

Javob

z.coerce.boolean() — Boolean(x). Boolean("false") — true (bo'sh bo'lmagan har satr truthy). Ya'ni HSTS=false HSTS'ni yoqardi — env darsidagi tuzoqning aynan o'zi. Shuning uchun aniq enum(["true","false"]) + transform.

3.4 Kalitlararo qoida: refine

"Production'da CORS_ORIGINS majburiy" — bu bitta kalitning emas, ikki kalit orasidagi qoida. Obyekt darajasida .refine:

ts
.refine((e) => e.NODE_ENV !== "production" ||
    e.CORS_ORIGINS.length > 0,
  { error: "production'da CORS_ORIGINS majburiy",
    path: ["CORS_ORIGINS"] });

Kanon jonli chiqishi (tekshiruv-40.txt):

text
$ NODE_ENV=production node src/server.ts
Sozlama noto'g'ri:
✖ production'da CORS_ORIGINS majburiy
  → at CORS_ORIGINS
[exit 78]

Kanon topilmasi (halol): obyekt .refine faqat qolgan maydonlar yaroqli bo'lsa ishlaydi. PORT=yuz NODE_ENV=production da faqat PORT xatosi chiqadi — CORS haqidagi xabar yo'q. Biz ham tekshirdik:

text
{"PORT":"yuz","NODE_ENV":"production"} → Sozlama noto'g'ri: | ✖ Invalid input: expected number, received NaN |   → at PORT

Dasturchi PORT ni tuzatadi, qayta ishga tushiradi — va endi CORS xatosini oladi. Ikki urinish. Zod sababi: refine butun obyektni oladi, obyekt esa maydonlardan biri buzilsa umuman tayyor bo'lmaydi.

4. Tipli va muzlatilgan Config

4.1 Kanon src/config.ts

ts
export type Config = Readonly<z.output<typeof EnvSxemasi>>;

export class ConfigXatosi extends Error {
  constructor(xabar: string) {
    super(xabar);
    this.name = "ConfigXatosi";
  }
}

/** @param env odatda process.env; testda — oddiy obyekt */
export function configYukla(
    env: Record<string, string | undefined>): Config {
  const n = EnvSxemasi.safeParse(env);
  // qiymatlarning o'zi chiqarilmaydi (sir bo'lishi mumkin)
  if (!n.success) throw new ConfigXatosi(
    `Sozlama noto'g'ri:\n${z.prettifyError(n.error)}`);
  return Object.freeze(n.data);
}
  • z.output<typeof EnvSxemasi> — sxemadan chiqish turi: PORT: number, CORS_ORIGINS: string[], HSTS: boolean, TRUST_PROXY: boolean | number | string. Turni qo'lda yozmaymiz — sxema bilan tur hech qachon ajralib ketmaydi.
  • Readonly<…> va Object.freeze — tur darajasida ham, ishlash paytida ham o'zgarmas. config.PORT = 1 — TypeScript xatosi va qat'iy rejimda TypeError.
  • ConfigXatosi — oddiy Error emas: server.ts uni boshqa xatolardan ajratadi.

Muzlatish sayoz — CORS_ORIGINS massivi ichi muzlamaydi. Readonly ham faqat yuqori daraja. Kanon buni qabul qilgan: massivni hech kim o'zgartirmaydi. Chuqur muzlatish kerak bo'lsa — node-config darsidagi muzlat mashqi.

4.2 as const — o'zgarmas ro'yxat

Kodda ruxsat etilgan qiymatlar ro'yxati ham sxemada, ham boshqa joyda kerak bo'ladi:

ts
const LOG_DARAJALARI = ["fatal", "error", "warn", "info"] as const;
type LogDaraja = (typeof LOG_DARAJALARI)[number];

function darajaMi(x: string): x is LogDaraja {
  return (LOG_DARAJALARI as readonly string[]).includes(x);
}
const d: LogDaraja = "warn";
console.log(d, darajaMi("info"), darajaMi("verbose"));

Konsolda:

text
warn true false

as const siz turi string[] bo'lardi va LogDaraja — oddiy string. U bilan — "fatal" | "error" | "warn" | "info". z.enum(LOG_DARAJALARI) ham shu massivni qabul qiladi: bitta ro'yxat — ham tur, ham tekshiruv.

4.3 server.ts: exit 78

ts
let config: Config;
try {
  config = configYukla(process.env);
} catch (x) {
  if (!(x instanceof ConfigXatosi)) throw x;
  console.error(x.message);
  process.exit(78); // EX_CONFIG
}

Nega 78? Unix'da sysexits.h degan eski fayl bor — unda dasturlar kelishib olgan chiqish kodlari ro'yxati. Shu ro'yxatda 78 — EX_CONFIG, "sozlama xatosi". process darsida exit kodlarini ko'rgansiz: 1 — umumiy xato. Alohida kod nazoratchiga (yiqilgan jarayonni qayta ishga tushiradigan dastur — oldingi dars) va odamga aytadi: "qayta ishga tushirish foydasiz, sozlamani tuzating". Masalan, Linux'dagi nazoratchi systemd'da (33-qismda ko'ramiz) RestartPreventExitStatus=78 qatori shu kod bilan qayta urinishni to'xtatadi.

Bu yerda process.exit — to'g'ri: hali server yo'q, kutadigan ish yo'q. console.error — chunki logger ham config'dan yasaladi va hali mavjud emas.

Kanon jonli chiqishi (t36-config.txt):

text
$ PORT=yuz SAQLASH=mongo NODE_ENV=production node src/server.ts
exit 78
Sozlama noto'g'ri:
✖ Noto‘g‘ri kirish: kutilgan raqam, qabul qilingan NaN
  → at PORT
✖ Noto‘g‘ri variant: quyidagilardan biri kutilgan "xotira"|"fayl"
  → at SAQLASH

Xabarlar o'zbekcha — Zod darsida yoqilgan z.locales.uz() tufayli (lokal o'z apostrofi ‘ ni ishlatadi). Biz bir nozik joyni ko'rdik: config.ts ni alohida sinasangiz, xabarlar inglizcha chiqadi. z.config(z.locales.uz()) — global sozlama va u tekshir.ts ichida. server.ts da u statik importlar orqali configYukla chaqirilishidan oldin yuklanadi; config testi esa tekshir.ts ni import qilmaydi. Shuning uchun kanon testi xabar matniga emas, at PORT ga tayanadi.

5. Biz topgan ikki teshik

Holat 40 config'ini npm ci dan keyin chekka qiymatlar bilan sinadik (Node 24.21.0). Har qatorda chapda — berilgan env, o'ngda — sxema nima qaytargani. Birinchi va oxirgi qatorga e'tibor bering: xato kutgan edik, lekin sxema jim qabul qildi:

text
{"PORT":""} → PORT 0 HSTS false CORS []
{"PORT":" "} → PORT 0 HSTS false CORS []
{"PORT":"3000abc"} → Sozlama noto'g'ri: | ✖ Invalid input: expected number, received NaN |   → at PORT
{"CORS_ORIGINS":"http://localhost:5173/"} → PORT 3000 HSTS false CORS ["http://localhost:5173/"]

Birinchi: PORT= — env darsidagi tuzoq sxemadan ham o'tdi. z.coerce.number() — bu Number(x), Number("") esa 0; min(0) uni qabul qiladi. Server tasodifiy portda. Davosi — bo'sh satrni "yo'q" deb hisoblash: z.preprocess((x) => (x === "" ? undefined : x), …) yoki min(1) (testlar PORT=0 ni sxema orqali emas, to'g'ridan beradi).

Ikkinchi: CORS_ORIGINS oxirida / — yaroqli URL, sxema qabul qildi. Lekin brauzer Origin sarlavhasini hech qachon / bilan yubormaydi. Biz tekshirdik:

text
CORS_ORIGINS=http://localhost:5173 → 200, Access-Control-Allow-Origin: http://localhost:5173
CORS_ORIGINS=http://localhost:5173/ → 200, Access-Control-Allow-Origin: null

Server 200 qaytardi, lekin ruxsat sarlavhasi yo'q — brauzer javobni bloklaydi. Bitta belgi — va frontend butunlay ishlamaydi, server logida esa hech narsa yo'q. Davosi — new URL(o).origin bilan normallashtirish (2-mashq).

Ikkala teshik ham kanonda tuzatilmagan — o'quvchi mashqi sifatida qoldi. Saboq: sxema yozildi — bu "endi hammasi tekshirildi" degani emas. Chekka qiymatlar bilan test yozing.

6. Tayyor kutubxonalar

6.1 Uch yo'l

vazifalar-api bu kutubxonalarni ishlatmaydi. Biz envalid 8.2.0 va @t3-oss/env-core 0.13.11 ni alohida papkada o'rnatib, bir xil yomon env bilan sinadik: PORT=yuz, NODE_ENV=prod, CORS_ORIGIN=maxfiy-qiymat. Chiqishlarda bitta narsaga qarang: maxfiy-qiymat matni xabarda ko'rinadimi?

envalid (cleanEnv):

text
================================
 Invalid environment variables:
    CORS_ORIGIN: Invalid url: "maxfiy-qiymat"
    NODE_ENV: Value "prod" not in choices [development,production,test]
    PORT: Invalid port input: "yuz"
================================

 Exiting with error code 1

Chiroyli (asl chiqishda ranglar bor) va hamma xato birdaniga. Lekin qiymatlar xabarda: "maxfiy-qiymat" ko'rinib turibdi. Agar bu DATABASE_URL bo'lsa — parol CI logida. Sukut bo'yicha process.exit(1) ham qiladi (78 emas). Yaxshi tomoni: natija muzlatilgan, e.PORT = 1 → TypeError [envalid] Attempt to mutate environment value: PORT, yo'q kalit → ReferenceError [envalid] Env var not found: YOQ.

@t3-oss/env-core — Zod (yoki validatorlar darsidagi Standard Schema'ni qo'llaydigan boshqa kutubxona) sxemasi ustiga yupqa qatlam:

text
❌ Invalid environment variables: [ … message: 'Invalid URL' … ]
tashladi: Error | Invalid environment variables

U qiymatni chiqarmadi (faqat received: 'NaN' kabi tur ma'lumoti). Asosiy xususiyati — clientPrefix: PUBLIC_ bilan boshlanmagan o'zgaruvchini brauzer kodida ishlatishga urinsangiz, xato beradi. Next.js/Vite bilan to'liq loyihalarda foydali (Next.js env darsidagi server-only g'oyasining kutubxona ko'rinishi).

O'z config.ts (kanon) envalid env-core
Bog'liqlik Zod (bor edi) +1 +1 (+ Zod)
Qiymatlar xabarda yo'q bor yo'q
Chiqish kodi 78 (o'zimiz) 1 xato tashlaydi
Client/server ajratish yo'q yo'q bor

Qaror (kanon): o'z config.ts — Zod allaqachon bor, 40 qator kod, qiymatlar yashirin.

7. API qadami: tipli config (holat 40, 3-qism)

Hali commit qilmang — commit sana va vaqt darsida.

Fayl O'zgarish
src/config.ts Yangi: EnvSxemasi, Config, ConfigXatosi, configYukla
src/server.ts Faqat configYukla(process.env); exit 78; omborYarat(config)
src/ilova.ts trustProxy, hsts, tz — Bogliqliklar orqali
test/config.test.ts 2 test: sukutlar/turlar/muzlatish va xatolar

Kanon sxemasidagi 12 kalit: NODE_ENV, PORT, HOST, CORS_ORIGINS, LOG_LEVEL, SAQLASH, FAYL_YOLI, TRUST_PROXY, APP_TZ, WEBHOOK_URL, HSTS va production qoidasi. TRUST_PROXY — reverse proxy darsidagi trust proxy: "false"/"true"/son/satr — transform bilan to'g'ri turga. APP_TZ — Intl.supportedValuesOf("timeZone") ro'yxatida bo'lishi kerak (sana-vaqt darsi).

Kanon fayllari to'liq (holat 40; qatorlar telefon uchun Prettier bilan bo'lindi, kod aynan). src/config.ts uzun, lekin tuzilishi oddiy: avval Rost yordamchisi, keyin bitta katta z.object (har kalit — bitta qism), oxirida obyekt darajasidagi .refine va configYukla. Har kalitda uch narsani qidiring: turi, sukuti, aylantirishi:

ts
// config.ts — muhit o'zgaruvchilaridan TIPLI va MUZLATILGAN config
// obyekti. Fail-fast: noto'g'ri sozlama bilan ilova ishga TUSHMAYDI.
// process.env faqat shu faylda o'qiladi — qolgan kod config'ni
// parametr sifatida oladi (testda boshqa qiymat berish oson)
import { z } from "zod";

const Rost = z
  .enum(["true", "false"])
  .transform((x) => x === "true");

const EnvSxemasi = z
  .object({
    NODE_ENV: z
      .enum(["development", "production", "test"])
      .default("development"),
    PORT: z.coerce.number().int().min(0).max(65_535).default(3000),
    // Sukut — faqat lokal: reverse proxy orqasida Node porti
    // tashqariga ochilmasin
    HOST: z.string().min(1).default("127.0.0.1"),
    CORS_ORIGINS: z
      .string()
      .default("")
      .transform((x) =>
        x
          .split(",")
          .map((o) => o.trim())
          .filter((o) => o !== ""),
      )
      .pipe(
        z.array(
          z.url({
            error:
              "CORS_ORIGINS — vergul bilan ajratilgan URL'lar (https://login.github.io)",
          }),
        ),
      ),
    LOG_LEVEL: z
      .enum([
        "fatal",
        "error",
        "warn",
        "info",
        "debug",
        "trace",
        "silent",
      ])
      .default("info"),
    SAQLASH: z.enum(["xotira", "fayl"]).default("xotira"),
    FAYL_YOLI: z.string().min(1).default("data/vazifalar.json"),
    // Express "trust proxy": false | true | proxy'lar soni |
    // "loopback"
    TRUST_PROXY: z
      .string()
      .default("false")
      .transform((x): boolean | number | string =>
        x === "true"
          ? true
          : x === "false"
            ? false
            : /^\d+$/.test(x)
              ? Number(x)
              : x,
      ),
    /**
     * foydalanuvchiga ko'rsatiladigan sana uchun mintaqa; saqlash —
     * doim UTC
     */
    APP_TZ: z
      .string()
      .default("Asia/Tashkent")
      .refine(
        (tz) =>
          Intl.supportedValuesOf("timeZone").includes(tz) ||
          tz === "UTC",
        { error: "APP_TZ — IANA mintaqa (Asia/Tashkent)" },
      ),
    /**
     * vazifa bajarilganda POST yuboriladigan manzil (ixtiyoriy)
     */
    WEBHOOK_URL: z.url().optional(),
    HSTS: Rost.default(false),
  })
  .refine(
    (e) => e.NODE_ENV !== "production" || e.CORS_ORIGINS.length > 0,
    {
      error: "production'da CORS_ORIGINS majburiy",
      path: ["CORS_ORIGINS"],
    },
  );

export type Config = Readonly<z.output<typeof EnvSxemasi>>;

export class ConfigXatosi extends Error {
  constructor(xabar: string) {
    super(xabar);
    this.name = "ConfigXatosi";
  }
}

/** @param env odatda process.env; testda — oddiy obyekt */
export function configYukla(
  env: Record<string, string | undefined>,
): Config {
  const n = EnvSxemasi.safeParse(env);
  // z.prettifyError — odam o'qiydigan ro'yxat (har xato alohida
  // qatorda, "at PORT" bilan). Qiymatlarning o'zi chiqarilmaydi (sir
  // bo'lishi mumkin)
  if (!n.success)
    throw new ConfigXatosi(
      `Sozlama noto'g'ri:\n${z.prettifyError(n.error)}`,
    );
  return Object.freeze(n.data);
}

src/server.ts:

ts
// server.ts — kirish nuqtasi: config (fail-fast) → ombor → server →
// jarayon hodisalari
// Ishga tushirish: node --env-file-if-exists=.env src/server.ts (npm
// start)
import { configYukla, ConfigXatosi } from "./config.ts";
import { ishgaTushir, omborYarat } from "./ishga-tushir.ts";
import { jarayonniBoshqar } from "./jarayon.ts";
import type { Config } from "./config.ts";

let config: Config;
try {
  config = configYukla(process.env);
} catch (x) {
  if (!(x instanceof ConfigXatosi)) throw x;
  console.error(x.message);
  // EX_CONFIG (sysexits.h) — supervisor qayta-qayta ishga
  // tushirmasin, sozlamani tuzatish kerak
  process.exit(78);
}

const { server, manzil } = await ishgaTushir({
  port: config.PORT,
  host: config.HOST,
  bogliqliklar: {
    ombor: await omborYarat(config),
    corsOriginlar: config.CORS_ORIGINS,
    tz: config.APP_TZ,
    trustProxy: config.TRUST_PROXY,
    hsts: config.HSTS,
  },
});
console.log(
  `vazifalar-api: ${manzil} (${config.NODE_ENV}, saqlash: ${config.SAQLASH})`,
);
jarayonniBoshqar(server, {
  info: (x) => {
    console.log(x);
  },
  fatal: (x, xato) => {
    console.error(x, xato);
  },
});

src/ilova.ts (holat 40 yakuni — tz, trustProxy, hsts bog'liqliklari bilan):

ts
// ilova.ts — Express ilovasi: middleware tartibi → routerlar → 404 →
// xato ishlovchisi.
// listen() YO'Q — server ishga-tushir.ts da, testlar ilovani portsiz
// ham ishlata oladi
import express from "express";
import cors from "cors";
import helmet from "helmet";
import { join } from "node:path";
import { sorovId } from "./umumiy/sorov-id.ts";
import {
  idempotentlik,
  XotiraIdempotentlikOmbori,
} from "./umumiy/idempotentlik.ts";
import type { IdempotentlikOmbori } from "./umumiy/idempotentlik.ts";
import { JSON_TURLARI } from "./umumiy/json-kerak.ts";
import { eskirgan, ESKI_YOLLAR } from "./umumiy/eskirgan.ts";
import {
  topilmadi,
  xatoIshlovchi,
} from "./umumiy/xato-ishlovchi.ts";
import type { XatoLogi } from "./umumiy/xato-ishlovchi.ts";
import { vazifaXizmati } from "./vazifalar/xizmat.ts";
import {
  teglarRouteri,
  vazifalarRouteri,
} from "./vazifalar/marshrut.ts";
import { eksportRouteri } from "./eksport/eksport.ts";
import type { VazifaOmbori } from "./vazifalar/ombor.ts";

export type Bogliqliklar = {
  ombor: VazifaOmbori;
  /**
   * CORS ruxsat etilgan origin'lar (frontend: Vite dev, Pages) —
   * bo'sh massiv: CORS sarlavhasi yo'q
   */
  corsOriginlar?: readonly string[];
  /** Idempotency-Key javoblari (27-qismda — Redis) */
  idempotentlikOmbori?: IdempotentlikOmbori;
  /**
   * kutilmagan xatolarni yozish (sukut — console.error)
   */
  xatoLogi?: XatoLogi;
  /** foydalanuvchi sanalari mintaqasi (?dan=, ?gacha=) */
  tz?: string;
  /** Express "trust proxy": false — proxy yo'q */
  trustProxy?: boolean | number | string;
  /**
   * HSTS faqat HTTPS orqasida (proxy) yoqiladi — lokal http'da
   * brauzerni chalg'itmasin
   */
  hsts?: boolean;
};

export function ilovaYarat({
  ombor,
  corsOriginlar = [],
  idempotentlikOmbori = new XotiraIdempotentlikOmbori(),
  xatoLogi,
  tz = "Asia/Tashkent",
  trustProxy = false,
  hsts = false,
}: Bogliqliklar): express.Express {
  const app = express();
  app.set("trust proxy", trustProxy);
  app.disable("x-powered-by"); // "Express" ekanini oshkor qilmaymiz
  app.use(sorovId());
  // helmet: nosniff, HSTS, CSP va h.k. — JSON API uchun ham arzon
  // (1 paket, import 2 ms)
  app.use(helmet({ strictTransportSecurity: hsts }));
  // CORS: frontend boshqa origin'dan (GitHub Pages) — faqat
  // ro'yxatdagilar; preflight (OPTIONS) shu yerda javob oladi
  app.use(
    cors({
      origin: [...corsOriginlar],
      allowedHeaders: [
        "Content-Type",
        "Idempotency-Key",
        "X-Request-Id",
        "If-Match",
        "If-None-Match",
      ],
      exposedHeaders: [
        "Location",
        "X-Request-Id",
        "Idempotency-Replayed",
        "ETag",
        "Link",
        "Deprecation",
        "Sunset",
      ],
      maxAge: 600,
    }),
  );
  app.use(
    express.static(join(import.meta.dirname, "..", "public")),
  );
  app.use(express.json({ limit: "16kb", type: JSON_TURLARI }));
  const xizmat = vazifaXizmati(ombor);
  // Versiya URI'da: /v1/... Buzuvchi o'zgarish (maydon nomi, javob
  // shakli) bo'lsa — /v2, v1 bir muddat yashaydi
  const v1 = express.Router();
  v1.use(
    "/vazifalar",
    idempotentlik(idempotentlikOmbori),
    vazifalarRouteri(xizmat, { tz }),
  );
  v1.use("/teglar", teglarRouteri(xizmat));
  v1.use("/eksportlar", eksportRouteri(ombor));
  app.use("/v1", v1);
  // Eski versiyasiz yo'llar (holat 05–24) — ishlaydi, lekin eskirgan:
  // Deprecation + Sunset + vorisga havola
  app.use(eskirgan(ESKI_YOLLAR), v1);
  app.use(topilmadi);
  app.use(xatoIshlovchi(xatoLogi));
  return app;
}

Diqqat: Oraliq kesim. Yuqoridagilar — holat 40 ning yakuniy fayllari. Ulardagi uch narsa sana-vaqt darsidagi fayllarga tayanadi: omborYarat (ishga-tushir.ts), tz (marshrut.ts va sorov.ts). Bugungi kesimda: server.ts da omborYarat(config) o'rniga new XotiraOmbori() (va uning importi), tz: config.APP_TZ qatorisiz; ilova.ts da tz maydoni va vazifalarRouteri(xizmat, { tz }) o'rniga vazifalarRouteri(xizmat). Biz shu kesimni tekshirdik: tip 0, lint 0, testlar 51/51.

test/config.test.ts — qiymat oshkor qilinmasligini tekshiradi:

ts
import { test } from "node:test";
import assert from "node:assert/strict";
import { configYukla, ConfigXatosi } from "../src/config.ts";

test("config: sukut qiymatlar, turlar, muzlatilgan", () => {
  const c = configYukla({});
  assert.equal(c.PORT, 3000);
  assert.equal(c.HOST, "127.0.0.1");
  assert.deepEqual(c.CORS_ORIGINS, []);
  assert.equal(c.SAQLASH, "xotira");
  assert.equal(c.TRUST_PROXY, false);
  assert.equal(c.APP_TZ, "Asia/Tashkent");
  assert.ok(Object.isFrozen(c));
  const d = configYukla({
    PORT: "4100",
    CORS_ORIGINS: "http://localhost:5173, https://login.github.io",
    TRUST_PROXY: "1",
    HSTS: "true",
  });
  assert.equal(d.PORT, 4100);
  assert.deepEqual(d.CORS_ORIGINS, [
    "http://localhost:5173",
    "https://login.github.io",
  ]);
  assert.equal(d.TRUST_PROXY, 1);
  assert.equal(d.HSTS, true);
});

test("config: noto'g'ri qiymat — ConfigXatosi, hammasi bitta xabarda (prettifyError), qiymat oshkor qilinmaydi", () => {
  assert.throws(
    () =>
      configYukla({
        PORT: "yuz",
        SAQLASH: "mongo",
        CORS_ORIGINS: "maxfiy-qiymat",
        APP_TZ: "Toshkent",
      }),
    (x: unknown) => {
      assert.ok(x instanceof ConfigXatosi);
      for (const nom of [
        "PORT",
        "SAQLASH",
        "CORS_ORIGINS",
        "APP_TZ",
      ])
        assert.match(x.message, new RegExp(`at ${nom}`));
      assert.doesNotMatch(x.message, /maxfiy-qiymat/);
      return true;
    },
  );
  assert.throws(
    () => configYukla({ NODE_ENV: "production" }),
    /production'da CORS_ORIGINS majburiy/,
  );
});

Kimdir keyinroq xabarga "qulaylik uchun" qiymatni qo'shsa — test qizaradi.

8. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
HSTS=false, lekin yoqildi z.coerce.boolean() enum(["true","false"]) + transform
PORT= → tasodifiy port coerce.number("") → 0 Bo'shni undefined ga; min(1)
Frontend CORS'da bloklandi Origin oxirida / new URL(o).origin
Xatolar bittadan chiqadi Har kalit alohida if Bitta sxema, safeParse
CI logida parol Xabarda qiymat (envalid sukuti) prettifyError — qiymatsiz
Testlar bir-biriga ta'sir qiladi process.env testda o'zgartirilgan configYukla({ … })
Production xabari chiqmadi refine boshqa xato bor paytda ishlamaydi Bilib qo'ying; tuzatib qayta ishga tushiring

9. Mashqlar

Yechimlar kurs/mashqlar/25/37-env-validatsiya/ da.

1-mashq (oson): sxema nima qaytaradi?

Kanon sxemasi bilan (configYukla):

  • { HSTS: "true" } → HSTS —
  • { TRUST_PROXY: "1" } → TRUST_PROXY —
  • { HSTS: "yes" } →
  • { PORT: "" } → PORT —
Yechim
  • true — boolean (Rost transform).
  • 1 — son: /^\d+$/ bo'lsa Number(x) (proxy'lar soni).
  • xato — faqat "true"/"false"; exit 78.
  • 0 — biz topgan teshik: bo'sh satr Number("") → 0.

2-mashq (o'rta): origin'larni normallashtirish

originlar(matn) yozing: vergul bilan ajratilgan ro'yxatni oladi, har birini new URL(x).origin ga aylantiradi (oxirgi /, yo'l, katta harf yo'qoladi), takrorlarni olib tashlaydi. Yaroqsiz element bo'lsa — Error("CORS_ORIGINS[N] — URL emas") (qiymatsiz).

Yechim
js
function originlar(matn) {
  const qismlar = matn.split(",").map((x) => x.trim())
    .filter((x) => x !== "");
  const natija = qismlar.map((x, i) => {
    if (!URL.canParse(x)) {
      throw new Error(`CORS_ORIGINS[${i}] — URL emas`);
    }
    return new URL(x).origin;
  });
  return [...new Set(natija)];
}

console.log(originlar(
  "http://localhost:5173/, HTTPS://Example.com/app"));
console.log(originlar("http://localhost:5173,http://localhost:5173/"));
try {
  originlar("http://localhost:5173, maxfiy-qiymat");
} catch (x) {
  console.log(x.message);
}

Konsolda:

text
[ 'http://localhost:5173', 'https://example.com' ]
[ 'http://localhost:5173' ]
CORS_ORIGINS[1] — URL emas

origin — sxema + host + port, xuddi brauzer Origin sarlavhasida yuboradigan ko'rinish. URL.canParse — try/catch siz tekshiruv. Kanon sxemasida buni .transform ichiga qo'yish mumkin.

3-mashq (qiyin): kichik configYukla

Zod'siz configYukla(env) yozing: PORT (bo'sh — sukut 3000; butun son 1–65535), SAQLASH (xotira|fayl, sukut xotira), HSTS (true|false, sukut false). Hamma xatolarni yig'ib, bitta Error da ✖ … → at KALIT formatida tashlasin; qiymatlar xabarda bo'lmasin. Natija muzlatilgan.

Yechim
js
function configYukla(env) {
  const xatolar = [];
  const c = {};
  const bosh = (x) => x === undefined || x.trim() === "";

  if (bosh(env.PORT)) c.PORT = 3000;
  else {
    const p = Number(env.PORT);
    if (Number.isInteger(p) && p >= 1 && p <= 65535) c.PORT = p;
    else xatolar.push(["PORT", "1–65535 oralig'idagi butun son"]);
  }
  const saqlash = env.SAQLASH ?? "xotira";
  if (["xotira", "fayl"].includes(saqlash)) c.SAQLASH = saqlash;
  else xatolar.push(["SAQLASH", "xotira yoki fayl"]);
  const hsts = env.HSTS ?? "false";
  if (hsts === "true" || hsts === "false") c.HSTS = hsts === "true";
  else xatolar.push(["HSTS", "true yoki false"]);

  if (xatolar.length) {
    const qatorlar = xatolar.map(([k, x]) => `✖ ${x}\n  → at ${k}`);
    throw new Error(`Sozlama noto'g'ri:\n${qatorlar.join("\n")}`);
  }
  return Object.freeze(c);
}

console.log(configYukla({ PORT: "", HSTS: "true" }));
try {
  configYukla({ PORT: "0", SAQLASH: "mongo", HSTS: "ha" });
} catch (x) {
  console.log(x.message);
}

Konsolda:

text
{ PORT: 3000, SAQLASH: 'xotira', HSTS: true }
Sozlama noto'g'ri:
✖ 1–65535 oralig'idagi butun son
  → at PORT
✖ xotira yoki fayl
  → at SAQLASH
✖ true yoki false
  → at HSTS

Bu funksiya kanondagi ikki teshikdan holi: bo'sh PORT — sukut, 0 — xato. Lekin 3 kalit uchun 25 qator — 12 kalitda Zod nega qulay ekani ko'rinib turibdi.

4-mashq: API qadami

src/config.ts ni yozing (kanon sxemasi), server.ts ni configYukla + exit 78 ga o'tkazing, test/config.test.ts ga ikki test qo'shing. Tekshiring: PORT=yuz CORS_ORIGINS=xato npm start → ikki xato, exit 78; NODE_ENV=production npm start → CORS xatosi. Qo'shimcha: biz topgan PORT= teshigini yoping va test yozing. Commit qilmang.

Yechim

Kanon chiqishi (tekshiruv-40.txt):

text
$ PORT=yuz CORS_ORIGINS=xato node src/server.ts
Sozlama noto'g'ri:
✖ Noto‘g‘ri kirish: kutilgan raqam, qabul qilingan NaN
  → at PORT
✖ CORS_ORIGINS — vergul bilan ajratilgan URL'lar (https://login.github.io)
  → at CORS_ORIGINS[0]
[exit 78]

Teshik uchun: PORT: z.preprocess((x) => (x === "" ? undefined : x), z.coerce.number().int().min(0).max(65_535).default(3000)) va test assert.equal(configYukla({ PORT: "" }).PORT, 3000). Bu kanondan tashqari — o'z yaxshilanishingiz.

10. Real ishda

  • Har jiddiy backend shablonida (create-t3-app, NestJS ConfigModule + validatsiya — 30-qism) env sxemasi bor. Begona loyihada birinchi qaraladigan fayl — env.ts/config.ts.
  • Deploy pipeline'da "ilova ishga tushdimi" tekshiruvi aynan shu fail-fast'ga tayanadi: config xato — deploy qizil, eski versiya ishlashda.
  • Intervyu savollari: "Fail-fast config nima?", "Nega process.env ni kod bo'ylab o'qish yomon?", "Xato xabarida sozlama qiymatini chiqarsa bo'ladimi?"

Xulosa

  • Noto'g'ri sozlama — ishga tushmaslik sababi: ConfigXatosi → exit 78 (EX_CONFIG).
  • process.env faqat server.ts da; configYukla(env) — toza funksiya, testda oddiy obyekt.
  • Zod: coerce, enum, default, transform + pipe, refine; "true"/"false" — coerce.boolean emas.
  • z.output + Readonly + Object.freeze — tipli, o'zgarmas Config; as const — ro'yxatdan tur.
  • Xatolar hammasi birdaniga va qiymatsiz; envalid sukut bo'yicha qiymatni ko'rsatadi.
  • Biz topgan teshiklar: PORT= → 0, CORS_ORIGINS oxiridagi / — chekka holatlarga test yozing.

Keyingi dars: Boshqa config vositalari va formatlar — YAML, TOML, JSON5, INI; YAML'da NO nega false bo'ladi; cosmiconfig va convict.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Env validatsiyasi Zod bilan: tipli config obyekti va fail-fast — IlmHamroh