ููุจูู ุฅููุ
endpoint ูุงุญุฏ POST /orders ูู
ุญู ูุดุฑู. ุงูุทูุจ ูุงุฒู
ูููู ููู:
- ุนู ูู: ุงุณู ู ู 2 ูู 50 ุญุฑูุ ูู ูุจุงูู ู ุตุฑูุ ูุฅูู ูู ุงุฎุชูุงุฑู.
- ุฃุตูุงู: ู
ู 1 ูู 20 ุตููุ ูู ุตูู
skuู ู ูุงูู ุฉ ู ุนุฑููุฉ ูqtyinteger ู ู 1 ูู 10. - ุทุฑููุฉ ุฏูุน:
cashุฃูcardุฃูwallet. - ู ูุงุญุธุงุช: ุงุฎุชูุงุฑูุฉ ููุญุฏ 200 ุญุฑู.
- ูู
ููุด ุฃู ุญูู ุฒูุงุฏุฉ: ุนุดุงู ู
ุญุฏุด ูุจุนุช
isAdminุฃูdiscount.
ajv-lab/โโ package.jsonโโ schemas/โ โโ order.json โ ุงูุนูุฏโโ validate.js โ Ajv + middleware + ุชูุณูู ุงูู errorsโโ server.js โ Expressterminal
mkdir ajv-lab && cd ajv-lab && pnpm initpnpm pkg set type=module scripts.start="node server.js"pnpm add ajv ajv-formats expressmkdir schemasุงูู schema
schemas/order.json
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://koshary.example/schemas/order.json", "title": "CreateOrder", "type": "object", "properties": { "customer": { "$ref": "#/$defs/customer" }, "items": { "type": "array", "minItems": 1, "maxItems": 20, "items": { "$ref": "#/$defs/item" } }, "payment": { "enum": ["cash", "card", "wallet"] }, "notes": { "type": "string", "maxLength": 200 } }, "required": ["customer", "items", "payment"], "additionalProperties": false, "$defs": { "customer": { "type": "object", "properties": { "name": { "type": "string", "minLength": 2, "maxLength": 50 }, "phone": { "type": "string", "pattern": "^01[0125][0-9]{8}$" }, "email": { "type": "string", "format": "email", "maxLength": 254 } }, "required": ["name", "phone"], "additionalProperties": false }, "item": { "type": "object", "properties": { "sku": { "enum": ["koshary-small", "koshary-large", "rice-pudding"] }, "qty": { "type": "integer", "minimum": 1, "maximum": 10 } }, "required": ["sku", "qty"], "additionalProperties": false } }}ุงูู validator ูุงูู middleware
validate.js
import { readFileSync } from "node:fs";import Ajv from "ajv/dist/2020.js";import addFormats from "ajv-formats";// instance ูุงุญุฏ ููุชุทุจูู ูููexport const ajv = new Ajv({ allErrors: true, strict: true });addFormats(ajv);const load = (file) => JSON.parse(readFileSync(new URL(`./schemas/${file}`, import.meta.url)));ajv.addSchema(load("order.json"));// ูุญููู errors ุจุชุงุนุฉ Ajv ูุดูู ูุถูู ูุฑุฌุน ููู clientexport function formatErrors(errors = []) { return errors.map((e) => { let field = e.instancePath || "/"; if (e.keyword === "required") field = `${e.instancePath}/${e.params.missingProperty}`; if (e.keyword === "additionalProperties") field = `${e.instancePath}/${e.params.additionalProperty}`; return { field, rule: e.keyword, message: e.message }; });}// middleware: ุจูุงุฎุฏ ุงูู $id ุจุชุงุน ุงูู schemaexport function validateBody(schemaId) { const validate = ajv.getSchema(schemaId); if (!validate) throw new Error(`schema not found: ${schemaId}`); return (req, res, next) => { if (validate(req.body)) return next(); res.status(400).json({ error: "ValidationError", details: formatErrors(validate.errors) }); };}- ุงูู
getSchemaุจุฑู ุงูู middleware: ุจูุญุตู ู ุฑุฉ ูุงุญุฏุฉ ูู ุง ุงูู route ูุชุณุฌูุ ู ุด ู ุน ูู request. - ุงูู
throwูู ุงูู schema ู ุด ู ูุฌูุฏุฉ: ุงูุณูุฑูุฑ ููุน ููุช ุงูู startup ุฃุญุณู ู ุง ููุน ู ุน ุฃูู request. - ุงูู
formatErrors: ุจูุตููุญ ู ูุงู ุงูุญูู ููrequiredูadditionalPropertiesุ ูุจูุฎุจููschemaPath.
ุงูุณูุฑูุฑ
server.js
import express from "express";import { validateBody } from "./validate.js";const app = express();app.use(express.json({ limit: "10kb" })); // ุญุฏ ููุญุฌู
ูุจู ุงูู validationapp.post("/orders", validateBody("https://koshary.example/schemas/order.json"), (req, res) => { // ููุง ุงูู body ู
ุถู
ูู ุดููู const count = req.body.items.reduce((sum, i) => sum + i.qty, 0); res.status(201).json({ ok: true, customer: req.body.customer.name, count });});// errors ุงูู JSON parser ููุณู (ูุจู ุงูู schema)app.use((err, req, res, next) => { if (err.type === "entity.parse.failed") return res.status(400).json({ error: "InvalidJSON" }); if (err.type === "entity.too.large") return res.status(413).json({ error: "PayloadTooLarge" }); next(err);});const port = process.env.PORT ?? 3000;app.listen(port, () => console.log(`listening on http://localhost:${port}`));- ุงูู
limit: "10kb": ุฃูู ุฎุท ุฏูุงุน. ุงูู body ุงูุถุฎู ุจูุชุฑูุถ ูุจู ู ุง ูุชุนู ูู parse ุฃู validation ุฎุงูุต. - ุงูู handler ุจูู ุจุณูุท: ู
ููุด ููุง
ifุนูู ุงูุดูู.req.body.items.reduceุขู ูุฉ ูุฃู ุงูู schema ุถู ูุช ุฅูitemsarray ูููุงqtyinteger.
ุฌุฑูุจ
terminal
pnpm start# ูู terminal ุชุงููH='content-type: application/json'U=localhost:3000/orders# 1) ุทูุจ ุณููู
curl -s -X POST $U -H "$H" -d '{"customer":{"name":"ุณุนูุฏ","phone":"01012345678"},"items":[{"sku":"koshary-large","qty":2},{"sku":"rice-pudding","qty":1}],"payment":"cash"}'# 2) ูู ุญุงุฌุฉ ุบูุทcurl -s -X POST $U -H "$H" -d '{"customer":{"name":"ุณ","phone":"123"},"items":[{"sku":"pizza","qty":"2"}],"payment":"bitcoin","isAdmin":true}'# 3) ุญููู ูุงูุตุฉcurl -s -X POST $U -H "$H" -d '{"customer":{"name":"ุณุนูุฏ"},"payment":"cash"}'# 4) JSON ุจุงูุธcurl -s -X POST $U -H "$H" -d '{bad json'# 5) ู
ู ุบูุฑ content-typecurl -s -X POST $U -d 'hello'ุงููุชุงูุฌ ุงูุญููููุฉ:
output
# 1{"ok":true,"customer":"ุณุนูุฏ","count":3}# 2{"error":"ValidationError","details":[ {"field":"/isAdmin","rule":"additionalProperties","message":"must NOT have additional properties"}, {"field":"/customer/name","rule":"minLength","message":"must NOT have fewer than 2 characters"}, {"field":"/customer/phone","rule":"pattern","message":"must match pattern \"^01[0125][0-9]{8}$\""}, {"field":"/items/0/sku","rule":"enum","message":"must be equal to one of the allowed values"}, {"field":"/items/0/qty","rule":"type","message":"must be integer"}, {"field":"/payment","rule":"enum","message":"must be equal to one of the allowed values"}]}# 3{"error":"ValidationError","details":[ {"field":"/items","rule":"required","message":"must have required property 'items'"}, {"field":"/customer/phone","rule":"required","message":"must have required property 'phone'"}]}# 4{"error":"InvalidJSON"}# 5{"error":"ValidationError","details":[{"field":"/","rule":"type","message":"must be object"}]}- ูู 2: ูฆ ุฃุฎุทุงุก ูู ุฑุฏ ูุงุญุฏ ุจูุถู
allErrorsุ ููู ูุงุญุฏ ููู ุงูู ูุงู ุจุงูุธุจุท. ูุงูู"qty": "2"ุงุชุฑูุถ ูุฃููุง ู ุดุบูููุงุดcoerceTypes. - ูู 3: ุงูู
fieldุจูู/itemsู/customer/phoneุจุฏู ุงูู parentุ ุจูุถูformatErrors. - ูู 5: Express 5 ู
ุจูุนู
ูุด parse ู
ู ุบูุฑ
content-type: application/jsonุ ูุงููreq.bodyุจููundefinedุ ูุงูู schema ุฑูุถุชู ุจูmust be object. ู ู ุบูุฑ ุงูู schema ูุงู ุงูู handler ูููุน. - ูbody ุฃูุจุฑ ู
ู 10kb: ุฑุฌุน
{"error":"PayloadTooLarge"}ุจู status413.
ููุณ ุงูุญูุงูุฉ ูู Fastify: built-in
Fastify ุจูุณุชุฎุฏู Ajv ุฌููุงูุ ูู ุด ู ุญุชุงุฌ middleware:
fastify.js
import Fastify from "fastify";const app = Fastify();app.post("/users", { schema: { body: { type: "object", properties: { name: { type: "string", minLength: 2 }, age: { type: "integer", minimum: 0 }, }, required: ["name"], additionalProperties: false, }, response: { 201: { type: "object", properties: { id: { type: "integer" }, name: { type: "string" } }, }, }, },}, async (req, reply) => { reply.code(201); return { id: 7, name: req.body.name, age: req.body.age, password: "secret123" };});await app.ready();for (const payload of [{ name: "Saeed", age: "30" }, { name: "S", isAdmin: true }, { age: 5 }]) { const r = await app.inject({ method: "POST", url: "/users", payload }); console.log(r.statusCode, r.body);}output
201 {"id":7,"name":"Saeed"}400 {"statusCode":400,"code":"FST_ERR_VALIDATION","error":"Bad Request","message":"body/name must NOT have fewer than 2 characters"}400 {"statusCode":400,"code":"FST_ERR_VALIDATION","error":"Bad Request","message":"body must have required property 'name'"}- ุงูู
schema.body: Fastify ุจูุนู ููุง compile ููุช ุงูู startupุ ูุจูุฑุฏ400ููุญุฏู. - ููู ูู
ุงู:
schema.querystringูschema.paramsูschema.headers. - ุงูู
schema.response: ุจูุชุนู ู ุจููุง serializer ุณุฑูุน ุจูfast-json-stringifyุจุฏูJSON.stringify. - ูุงุญุธ ุงูู response: ุงูู handler ุฑุฌูุน
passwordูageุ ุจุณ ุงูุฑุฏ ุทูุน{"id":7,"name":"Saeed"}ุจุณ. ุงูู serializer ุจูุทููุน ุงูุญููู ุงููู ูู ุงูู schema ุจุณ. ุฏู ุจูุญู ูู ู ู ุชุณุฑูุจ ุญููู ุจุงูุบูุท. - ุงูู response schema ู ุด validation: ูู ุจูุดููู ุงูู outputุ ู ุด ุจูุฑูุถู ูู ุบูุท. ู ุชุนุชู ุฏุด ุนููู ูู test.
fastify-strict.js
const app = Fastify({ ajv: { customOptions: { removeAdditional: false, coerceTypes: false } },});// {"name":"Saeed","isAdmin":true} โ 400 body must NOT have additional properties// {"name":"Saeed","age":"30"} โ 400 body/age must be integerุฎููุตุช ุงูู Lab ูู ุง
๐ง ุงุฎุชุจุฑ ููุณู
ูก) ููู getSchema ุจุฑู ุงูู function ุงููู ุจุชุฑุฌุน ููู middlewareุ
ุนุดุงู ูุญุตู ู
ุฑุฉ ูุงุญุฏุฉ ููุช ุชุณุฌูู ุงูู routeุ ูุงูู middleware ูุณุชุฎุฏู
ููุณ ุงูู function ู
ุน ูู request.
ูข) request ู
ู ุบูุฑ content-type ูุตู ุงูู handler ุจู req.body undefined. ุงูู schema ุนู
ูุช ุฅููุ
ุฑูุถุชู ุจู
must be objectุ ูุงูู handler ู
ุชููุฐุด ุฃุตููุง.ูฃ) ูู Fastify ุงูููุฒุฑ ุจุนุช role: admin ูุงูู schema ูููุง additionalProperties: falseุ ูู
ููุด error. ูููุ
ูุฃู Fastify ุดุบูุงู ุจู
removeAdditional: trueุ ูุงูุญูู ุงุชู
ุณุญ ุจูุฏูุก. ูู ุนุงูุฒ ุฑูุถุ ุฎููู false ูู customOptions.ูค) ุงูู handler ุฑุฌูุน passwordHash ุจุงูุบูุท. ุฅุฒุงู Fastify ูู
ูุนู ูุทูุนุ
ุจู
schema.response ู
ู ุบูุฑ ุงูุญูู ุฏูุ ูุฃู ุงูู serializer ุจูุทููุน ุงูุญููู ุงูู
ุฐููุฑุฉ ุจุณ.