Express.js-এ Zero-Boilerplate OpenAPI: Zod ও Fluent Routing দিয়ে এন্ড-টু-এন্ড টাইপ-সেফ ডকুমেন্টেশন ইঞ্জিন

작성자

카테고리:

← 피드로
DEV Community · Rasel Mahmud · 2026-09-05 개발(SW)

কেমন হতো যদি আমরা অত্যন্ত ক্লিন ও ডিক্লারেটিভ উপায়ে রাউট ডেফিনিশনের সাথেই Swagger ডক্স যুক্ত করতে পারতাম? যেখানে রাউটার নিজে থেকেই ভ্যালিডেশন মিডলওয়্যার থেকে সব ইনপুট স্কিমা (Query, Params, Body) রিড করে সম্পূর্ণ ভ্যালিড OpenAPI ডকুমেন্টেশন স্বয়ংক্রিয়ভাবে জেনারেট করে দেবে!

এই আর্কিটেকচারাল প্যাটার্ন ব্যবহারের ফলে এন্টারপ্রাইজ প্রজেক্টে এপিআই স্পেসিফিকেশন মেইনটেন্যান্স ওভারহেড প্রায় শূন্যে নেমে আসে। কোড রিফ্যাক্টরিংয়ের সময় ডকুমেন্টেশন ভেঙে যাওয়ার কোনো সুযোগ থাকে না, কারণ কোড এবং ডকস—দুটোই পরিচালিত হয় একই Zod ভ্যালিডেশন স্কিমা দ্বারা।

Express.js দিয়ে ব্যাকএন্ড আর্কিটেকচার দাঁড় করানো অত্যন্ত দ্রুত এবং ফ্লেক্সিবল। কিন্তু এন্টারপ্রাইজ লেভেলে যখন কোনো প্রজেক্টে ২০০ থেকে ৩০০+ এন্ডপয়েন্ট চলে আসে, তখন এর OpenAPI (Swagger) স্পেসিফিকেশন রক্ষণাবেক্ষণ করা ইঞ্জিনিয়ারিং টিমের জন্য সবচেয়ে বড় ফ্রাস্ট্রেশনের কারণ হয়ে দাঁড়ায়।

প্রচলিত সমাধানগুলো কেন প্রোডাকশন স্কেলে টেকসই নয়, এবং কীভাবে Zod-কে Single Source of Truth বানিয়ে সম্পূর্ণ Zero-Boilerplate উপায়ে অটোমেটিক ডক্স জেনারেট করা সম্ভব—তা নিয়ে এই বিস্তারিত আলোচনা:

১. প্রচলিত পদ্ধতির সীমাবদ্ধতা ও দুর্বলতা

Express ইকোসিস্টেমে সাধারণত কয়েকটি library দিয়ে OpenAPI ডকুমেন্টেশন তৈরি করা হয়, যার প্রতিটিতেই গুরুতর আর্কিটেকচারাল ট্রেড-অফ রয়েছে:

১. swagger-jsdoc (JSDoc YAML Comments)

প্রতিটি কন্ট্রোলারের মাথায় JSdocs এর মধ্যে দুর্বোধ্য YAML কমেন্ট লিখতে হয়।

/**
 * @swagger
 * /api/v1/users/{id}:
 *   get:
 *     summary: নির্দিষ্ট ইউজারের তথ্য আনুন
 *     tags: [Users]
 *     parameters:
 *       - in: path
 *         name: id
 *         required: true
 *         schema:
 *           type: string
 *           format: uuid
 *     responses:
 *       200:
 *         description: সফল রেসপন্স
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/UserResponse'
 */
router.get("/:id", userController.getUserById);

Enter fullscreen mode Exit fullscreen mode

  • সমস্যা কি? কোডের চেয়ে কমেন্টের দৈর্ঘ্য বেশি হয়ে যায় এবং কোনো কম্পাইল-টাইম টাইপ-সেফটি থাকে না। এছাড়া রিকোয়েস্ট বডি, পারামস কিংবা কুয়েরি ভ্যালিডেশন লজিকে একবার লিখতে হয়, আবার কমেন্ট ব্লকেও নতুন করে ডিফাইন করতে হয়—যার ফলে অপ্রয়োজনীয় ডুপ্লিকেশন তৈরি হয় এবং কোড পরিবর্তনের সাথে সাথে ডকুমেন্টেশন দ্রুত আউট-অফ-সিঙ্ক হয়ে যায়।

২. আইসোলেটেড swagger.json বা openapi.yaml স্পেক

কোডবেস থেকে আলাদা একটি বিশালাকার ফাইল মেইনটেইন করা।

  • সমস্যা কি?: স্কিমা ডুপ্লিকেশন। একই ফিল্ড রিকোয়েস্ট ভ্যালিডেশনে (যেমন Zod বা Joi) একবার লিখতে হয়, আবার JSON ফাইলে আরেকবার। টিমের একাধিক ডেভেলপার একসাথে কাজ করলে গিট মার্জ কনফ্লিক্ট অনিবার্য হয়ে পড়ে।

২. আর্কিটেকচারাল গোল: Zod is a ‘Single Source of Truth’

আমরা প্রতিটি এন্ডপয়েন্টে ইনকামিং ডেটা স্যানিটাইজ ও ভ্যালিডেট করার জন্য ইতোমধ্যে Zod ব্যবহার করি। আমাদের লক্ষ্য ছিল:

  1. ভ্যালিডেশন মিডলওয়্যার থেকেই স্বয়ংক্রিয়ভাবে স্কিমা extract করা।
  2. রাউটারের মেথড চেইনিং (.swagger({...})) ব্যবহার করে রুট-নির্দিষ্ট মেটাডাটা (যেমন: summary, description, tags, responses) সরাসরি রাউট ডেফিনিশনেই যুক্ত করা—যাতে আলাদা কোনো ফাইলে এগুলো মেইনটেইন করার প্রয়োজন না হয় এবং এক জায়গাতেই পুরো রাউটের ডকুমেন্টেশন পরিষ্কার থাকে।
  3. কোনো ডুপ্লিকেট কনফিগারেশন ছাড়া রানটাইমে বৈধ OpenAPI 3.0 স্পেক ও Swagger UI পরিবেশন করা।

আমরা zod-to-openapi এবং swagger-ui-express ব্যবহার করে নিচে দেখানো আর্কিটেকচার অনুযায়ী সিস্টেমটি তৈরি করেছি:

┌─────────────────────────────────────────────────────────────┐
│                     Express Route Layer                     │
│    router.post('/', ZodValidation.validate(...), handler)   │
└──────────────────────────────┬──────────────────────────────┘
                               │ .swagger(config)
                               ▼
┌─────────────────────────────────────────────────────────────┐
│                 SwaggerRouter Proxy Wrapper                 │
│   - Express রাউটার ইন্টারসেপ্ট করে স্ট্যাক ট্র্যাক করে        │
│   - Route লেয়ারের প্রোপার্টিতে swaggerConfig বাইন্ড করে    │
└──────────────────────────────┬──────────────────────────────┘
                               │
                               ▼
┌─────────────────────────────────────────────────────────────┐
│             Middleware Metadata Reflection                  │
│   - ভ্যালিডেশন মিডলওয়্যার স্ট্যাকে রিফ্লেকশন ফ্ল্যাগ বসায়:   │
│     (fn)._validation = { type: 'body' | 'query', schema }   │
└──────────────────────────────┬──────────────────────────────┘
                               │
                               ▼
┌─────────────────────────────────────────────────────────────┐
│               generateOpenApiDocument Engine                │
│   ১. নেস্টেড রাউটার স্ট্যাক রিকার্সিভলি অ্যানালাইজ করে      │
│   ২. এক্সপ্রেসের পাথ প্যারাম রূপান্তর করে (:id -> {id})    │
│   ৩. Zod স্কিমা থেকে OpenAPI স্কিমা ও প্যারামিটার কনভার্ট করে│
│   ৪. গ্লোবাল এরর (400, 401, 500) রেসপন্স মার্জ করে          │
└──────────────────────────────┬──────────────────────────────┘
                               │
                               ▼
┌─────────────────────────────────────────────────────────────┐
│              Swagger UI (/docs) & Spec (/docs.json)         │
└─────────────────────────────────────────────────────────────┘

Enter fullscreen mode Exit fullscreen mode

৩. কোর আর্কিটেকচার ও মেকানিজম (Core Mechanisms)

A. Fluent Router Wrapper (createSwaggerRouter.ts)

Express-এর সাধারণ রাউটারে ডিফল্টভাবে চেইনেবল মেটাডাটা যুক্ত করার কোনো ব্যবস্থা থাকে না। তাই আমরা Express Router-কে একটি হালকা র‍্যাপার দিয়ে ইন্টারসেপ্ট করি, যাতে প্রতিটি রাউট লেখার শেষেই সাবলীলভাবে .swagger({...}) মেথড চেইন করা যায়:

export const createSwaggerRouter = (): SwaggerRouter => {
    const router: any = express.Router();
    const methods = ["get", "post", "put", "delete", "patch"] as const;

    for (const method of methods) {
        const originalMethod = router[method].bind(router);

        router[method] = (path: string, ...handlers: RequestHandler[]) => {
            originalMethod(path, ...handlers);

            // 1. Capture the last added route layer
            const lastLayer = router.stack ? router.stack[router.stack.length - 1] : undefined;

            // 2. Return a chainable .swagger() method that will bind metadata to that layer
            return Object.assign(router, {
                swagger: (swaggerConfig: SwaggerConfig) => {
                    if (lastLayer?.route) {
                        lastLayer.route.swaggerConfig = swaggerConfig;
                    }
                    return router;
                },
            });
        };
    }

    return router as SwaggerRouter;
};

Enter fullscreen mode Exit fullscreen mode

কেন আমরা মেথড চেইনিং (.swagger()) বেছে নিলাম?

রাউট ডেফিনিশন এবং তার ডকুমেন্টেশন মেটাডাটা (যেমন: summary, description, tags, resDto) ঠিক একই জায়গায় কো-লোকেটেড (Co-located) রাখার জন্যই মূলত এই চেইনিং মেকানিজম ব্যবহার করা হয়েছে। এতে ডেভেলপারদের আলাদা কোনো ফাইলে গিয়ে কনফিগ লিখতে হয় না এবং কোড পরিবর্তনের সাথে সাথেই চোখের সামনে ডকুমেন্টেশন আপডেট করে ফেলা যায়। এছাড়া NestJS বা TSOA-এর মতো ভারী কোনো Decorator সেটআপ ছাড়াই খাঁটি ভ্যানিলা Express.js-এর চিরাচরিত চেইনিং সিনট্যাক্স পুরোপুরি বজায় থাকে।

এটি কীভাবে কাজ করে?

যখনই কোনো ডেভেলপার router.patch() বা router.post() কল করেন, এটি প্রথমে এক্সপ্রেসের আসল মেথডটি এক্সিকিউট করে রাউটার স্ট্যাকে (router.stack) নতুন একটি রাউট লেয়ার তৈরি করে। এর পরপরই আমরা স্ট্যাকের সর্বশেষ পুশ হওয়া লেয়ারটি (lastLayer) ক্যাপচার করি এবং একটি চেইনেবল .swagger({...}) মেথড রিটার্ন করি। যখন এই মেথডটি এক্সিকিউট হয়, তখন তা সরাসরি ওই lastLayer.route-এর ভেতরে swaggerConfig মেটাডাটা bind করে দেয়—যা পরবর্তীতে আমাদের spec জেনারেটর ইঞ্জিন স্বয়ংক্রিয়ভাবে রিড করতে পারে।

B. Middleware Metadata Reflection (ZodValidation.ts)

রিকোয়েস্ট মেথড এর স্কিমা (Query, Params, Body) দ্বিতীয়বার না লেখার জন্য ভ্যালিডেশন মিডলওয়্যার এক্সিকিউশনের সময় স্কিমাটি ফাংশন অবজেক্টে মেটাডাটা হিসেবে সংরক্ষিত থাকে:

import { Request, Response, NextFunction } from "express";
import { ZodTypeAny } from "zod";

export class ZodValidation {
    static validateBody(schema: ZodTypeAny) {
        const fn = (req: Request, res: Response, next: NextFunction) => {
            const result = schema.safeParse(req.body);
            if (!result.success) {
                throw new Exception({
                    ....
                })
            }
            req.body = result.data;
            next();
        };

        // Metadata Property Tagging
        fn._validation = { type: "body", schema };
        return fn;
    }

    static validateQuery(schema: ZodTypeAny) {
        const fn = (req: Request, res: Response, next: NextFunction) => {
            const result = schema.safeParse(req.query);
            if (!result.success) {
                return res.status(400).json({
                    code: "QUERY_VALIDATION_ERROR",
                    errors: result.error.flatten(),
                });
            }
            req.query = result.data;
            next();
        };

        fn._validation = { type: "query", schema };
        return fn;
    }
}

Enter fullscreen mode Exit fullscreen mode

জাভাস্ক্রিপ্টে প্রতিটি ফাংশনই মূলত একটি ফার্স্ট-ক্লাস অবজেক্ট (First-Class Object), যার কারণে যেকোনো ফাংশন ইনস্ট্যান্সে সহজেই কাস্টম প্রোপার্টি অ্যাসাইন করা যায়। যখনই আমরা ZodValidation.validateBody(schema) বা validateQuery(schema) কল করি, তখন তা রিটার্ন হওয়া মিডলওয়্যার ফাংশনটির গায়ে fn._validation = { type: 'body', schema } মেটাডাটা প্রোপার্টি ট্যাগ করে দেয়।

এর বড় সুবিধা হলো—সাধারণ রিকোয়েস্ট চলাকালীন মিডলওয়্যারটি এক্সপ্রেসের স্বাভাবিক নিয়মে ডেটা স্যানিটাইজ ও ভ্যালিডেট করে কোনো অতিরিক্ত রানটাইম ওভারহেড তৈরি করে না। অন্যদিকে অ্যাপ্লিকেশন স্টার্ট হওয়ার সময় আমাদের ডকুমেন্টেশন ইঞ্জিন রাউটের মিডলওয়্যার স্ট্যাক ট্রাভার্স করে সরাসরি এই _validation অবজেক্ট থেকে Zod স্কিমাটি তুলে নেয়। ফলে একই স্কিমা Swagger-এর জন্য আলাদা ফাইলে বা আলাদা কনফিগারেশনে পুনরায় লেখার প্রয়োজন সম্পূর্ণ দূর হয়।

C. OpenAPI Spec Generation & Registry Engine (generateOpenApiDocument.ts)

পুরো সিস্টেমের মূল ইঞ্জিন হলো generateOpenApiDocument ফাংশন। এটি এক্সপ্রেসের ইন্টারনাল রাউটিং স্ট্যাক অ্যানালাইজ করে, রিফ্লেকশন মেটাডাটা থেকে স্কিমা তুলে নেয় এবং @asteasolutions/zod-to-openapi এর মাধ্যমে সম্পূর্ণ Type-Safe OpenAPI স্পেক জেনারেট করে:

import { OpenAPIRegistry, OpenApiGeneratorV3, extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi';

export function generateOpenApiDocument(app: Application, globalDocConfig: any) {
    const registry = new OpenAPIRegistry();

    // 1. Recursively traverse Express router stack to extract all routes and metadata
    const routes = extractAllRoutes(app);

    for (const { path, method, swaggerConfig, fallbackTag } of routes) {
        const { summary, description, tags, reqDto, resDto, responses } = swaggerConfig;

        // 2. Bind extracted request body, params, and query schemas
        const request = buildRequestObject(reqDto, globalDocConfig.request);

        // 3. Merge route-specific responses with global default error responses (400, 401, 404, etc.)
        const finalResponses = mergeResponses(responses, resDto, globalDocConfig.responses);

        // 4. Register the endpoint into OpenAPIRegistry
        registry.registerPath({
            method: method as any,
            path: path, // Express ":id" -> OpenAPI "{id}"
            summary,
            description,
            tags: tags || [fallbackTag],
            request: Object.keys(request).length > 0 ? request : undefined,
            responses: finalResponses,
        });
    }

    // 5. Generate final OpenAPI 3.0 document from registry
    const generator = new OpenApiGeneratorV3(registry.definitions);
    return generator.generateDocument(globalDocConfig);
}

Enter fullscreen mode Exit fullscreen mode

ডকুমেন্ট জেনারেশনের শুরুতেই ইঞ্জিনটি এক্সপ্রেস app এবং নেস্টেড সাব-রাউটারগুলোর ইন্টারনাল স্ট্যাক (_router.stack) রিকার্সিভলি স্ক্যান করে প্রতিটি এন্ডপয়েন্ট খুঁজে বের করে এবং এক্সপ্রেসের পাথ প্যারামিটার ফরম্যাটকে (:id) স্বয়ংক্রিয়ভাবে OpenAPI ফরম্যাটে ({id}) রূপান্তর করে। এরপর প্রতিটি রাউটের মিডলওয়্যার স্ট্যাক ঘুরে fn._validation মেটাডাটা থেকে রিকোয়েস্টের body, query, এবং params স্কিমাগুলো সরাসরি সংগ্রহ করা হয়।

পরবর্তীতে রাউট-লেভেলের কাস্টম মেটাডাটা (tags, summary, description, resDto) এবং গ্লোবাল ডিফল্ট কনফিগারেশন (যেমন: কমন এরর রেসপন্স 400, 401, 404 এবং Auth কুকিজ/হেডার্স) নিখুঁতভাবে মার্জ করে @asteasolutions/zod-to-openapi-এর OpenAPIRegistry-তে প্রতিটি পাথ রেজিস্টার করা হয়। সব শেষে OpenApiGeneratorV3 ক্লাস পুরো রেজিস্ট্রি ডেটাকে কম্পাইল করে রানটাইমে ১০০% ভ্যালিড OpenAPI 3.0 স্পেক তৈরি করে, যা কোনো এক্সটার্নাল ফাইল ছাড়াই স্বয়ংক্রিয়ভাবে Swagger UI-তে রেন্ডার হয়ে যায়।

4. গ্লোবাল কনফিগারেশন ও ডিফল্ট রেসপন্স হ্যান্ডলিং (swaggerInit.ts)

এন্টারপ্রাইজ অ্যাপ্লিকেশনে প্রতিটি এন্ডপয়েন্টে আলাদাভাবে কমন এরর রেসপন্স (যেমন: 400, 401, 403, 404) বা কমন সিকিউরিটি কুকিজ/হেডার্স ডিফাইন করা অত্যন্ত ক্লান্তিকর ও পুনরাবৃত্তিমূলক। আমাদের আর্কিটেকচারে অ্যাপ্লিকেশন বুটস্ট্র্যাপ করার সময় একবার গ্লোবাল কনফিগারেশন সেট করে দেওয়া হয়, যা স্বয়ংক্রিয়ভাবে সমস্ত রুটের ডকুমেন্টে মার্জ হয়ে যায়:

import { Application } from "express";
import swaggerUi from "swagger-ui-express";
import { z } from "zod";
import generateOpenApiDocument from "./generateOpenApiDocument";

export function swaggerInit(app: Application, isEnabled: boolean = false) {
    if (!isEnabled) return;

    const openApiDoc = generateOpenApiDocument(app, {
        disabled: false,
        openapi: "3.0.0",
        info: {
            title: "Enterprise Backend API",
            version: "1.0.0",
            description: "Production-ready automated OpenAPI documentation",
        },
        servers: [
            {
                url: "http://localhost:5000",
                description: "Local Development Server",
            },
        ],
        // Global default error response
        responses: {
            400: {
                description: "Bad Request - Validation Error",
                schema: z.object({
                    code: z.string(),
                    message: z.string(),
                    traceId: z.string().uuid(),
                }).openapi({
                    example: {
                        code: "VALIDATION_ERROR",
                        message: "Invalid payload provided",
                        traceId: "550e8400-e29b-41d4-a716-446655440000",
                    },
                }),
            },
            401: {
                description: "Unauthorized - Invalid or Expired Session",
                schema: z.object({
                    code: z.string(),
                    message: z.string(),
                    traceId: z.string().uuid(),
                }).openapi({
                    example: {
                        code: "UNAUTHORIZED",
                        message: "Authentication token is missing or expired",
                        traceId: "550e8400-e29b-41d4-a716-446655440000",
                    },
                }),
            },
            403: {
                description: "Forbidden - Insufficient Permissions",
                schema: z.object({
                    code: z.string(),
                    message: z.string(),
                    traceId: z.string().uuid(),
                }).openapi({
                    example: {
                        code: "FORBIDDEN",
                        message: "You do not have permission to perform this action",
                        traceId: "550e8400-e29b-41d4-a716-446655440000",
                    },
                }),
            },
            404: {
                description: "Resource Not Found",
                schema: z.object({
                    code: z.string(),
                    message: z.string(),
                    traceId: z.string().uuid(),
                }).openapi({
                    example: {
                        code: "NOT_FOUND",
                        message: "The requested resource could not be found",
                        traceId: "550e8400-e29b-41d4-a716-446655440000",
                    },
                }),
            },
        },
        // Global request cookies
        request: {
            cookies: z.object({
                auth_token: z.string().openapi({ example: "s%3AeyJhbGciOi..." }),
                auth_refresh_token: z.string().openapi({ example: "s%3AeyJhbGciOi..." }),
            }),
        },
    });

    if (openApiDoc) {
        app.use("/docs", swaggerUi.serve, swaggerUi.setup(openApiDoc));
        app.get("/docs.json", (req, res) => res.json(openApiDoc));
    }
}

Enter fullscreen mode Exit fullscreen mode

গ্লোবাল কনফিগারেশনে একবার স্ট্যান্ডার্ড এরর রেসপন্স স্কিমা এবং সিকিউরিটি কুকিজ ডিফাইন করে দেওয়ায় প্রতিটি ডেভেলপারের আলাদাভাবে এরর ডকুমেন্ট করার ঝামেলা পুরোপুরি মুছে যায়। ইঞ্জিন নিজে থেকেই প্রতিটি রুটের জন্য নির্ধারিত সাকসেস রেসপন্সের সাথে এই ডিফল্ট এরর রেসপন্সগুলো অ্যাটাচ করে পূর্ণাঙ্গ এপিআই স্পেসিফিকেশন তৈরি করে।

5. প্রোডাকশন ব্যবহার: এক্সপ্রেস অ্যাপে কানেক্ট করা ও এন্ড-টু-এন্ড ফ্লো

এবার আমরা দেখব কীভাবে সম্পূর্ণ আর্কিটেকচারটি একটি বাস্তব এক্সপ্রেস অ্যাপ্লিকেশনে প্লাগ করতে হয় এবং কীভাবে এন্ড-টু-এন্ড টাইপ-সেফ ডকুমেন্টেশন উৎপন্ন হয়।

১. এক্সপ্রেস অ্যাপে ডক্স ইঞ্জিন ইনজেক্ট করা (app.ts)

অ্যাপ্লিকেশনের সমস্ত মডিউলার রাউটার এক্সপ্রেস ইনস্ট্যান্সে মাউন্ট করার পর একদম শেষে শুধু swaggerInit(app, isEnabled) ফাংশনটি কল করতে হয়। এর ফলে এক্সপ্রেসের অভ্যন্তরীণ রাউটিং স্ট্যাক স্ক্যান সম্পন্ন হয়ে তাৎক্ষণিকভাবে /docs পাথে ইন্টারেক্টিভ Swagger UI এবং /docs.json পাথে কাঁচা OpenAPI 3.0 স্পেক পরিবেশন শুরু হয়ে যায়:

import express from "express";
import { swaggerInit } from "./swagger/swaggerInit";
import userRouter from "./modules/user/user.route";

const app = express();
app.use(express.json());

// Modular router mount
app.use("/api/v1/users", userRouter);

// Swagger initialization after registering all routes
swaggerInit(app, process.env.NODE_ENV !== "production");

export default app;

Enter fullscreen mode Exit fullscreen mode

২. Zod স্কিমা ডিফাইন করা (user.validation.ts)

ইনপুট ভ্যালিডেশন এবং রেসপন্স মডেল উভয়ের জন্যই Zod স্কিমা ডিফাইন করা হয়। @asteasolutions/zod-to-openapi-এর এক্সটেনশনের সাহায্যে আমরা স্কিমার ফিল্ডগুলোতে সমৃদ্ধ উদাহরণ (.openapi({ example: ... })) ও ডেসক্রিপশন যুক্ত করতে পারি:

import { z } from "zod";
import { extendZodWithOpenApi } from "@asteasolutions/zod-to-openapi";

extendZodWithOpenApi(z);

export class UserValidation {
    static UserSchema() {
        return z.object({
            id: z.string().uuid().openapi({ example: "550e8400-e29b-41d4-a716-446655440000" }),
            name: z.string().min(2).openapi({ example: "Tanvir Ahmed" }),
            email: z.string().email().openapi({ example: "[EMAIL_ADDRESS]" }),
            role: z.enum(["ADMIN", "USER"]).openapi({ example: "USER" }),
        });
    }

    static updateUserBodySchema() {
        return z.object({
            name: z.string().min(2).max(50).optional().openapi({ example: "Tanvir Ahmed" }),
            email: z.string().email().optional().openapi({ example: "[EMAIL_ADDRESS]" }),
            role: z.enum(["ADMIN", "USER"]).default("USER").openapi({ example: "ADMIN" }),
        });
    }

    static UpdateUserResponse() {
        return z.object({
            message: z.string().openapi({ example: "User updated successfully." }),
            data: this.UserSchema(),
        });
    }
}

Enter fullscreen mode Exit fullscreen mode

৩. ডিক্লারেটিভ রাউটিং ও মেথড চেইনিং (user.route.ts)

আমাদের কাস্টম createSwaggerRouter() ব্যবহার করে সাধারণ এক্সপ্রেস রাউটের মতোই মিডলওয়্যার ও কন্ট্রোলার পাস করা হয়। রাউট ডিক্লারেশনের শেষে সরাসরি .swagger({...}) মেথড চেইন করে শুধু এন্ডপয়েন্টের মেটাডাটা এবং সাকসেস রেসপন্স স্কিমাটি যুক্ত করে দেওয়া হয়:

import { createSwaggerRouter } from "../../swagger/createSwaggerRouter";
import { ZodValidation } from "../../middleware/ZodValidation";
import { commonValidation } from "../../validations/commonValidation";
import { UserValidation } from "./user.validation";
import { userController } from "./user.controller";

const router = createSwaggerRouter();

router.patch(
    "/users/update/:id",
    ZodValidation.validateParams(commonValidation.idUUid()),
    ZodValidation.validate(UserValidation.updateUserBodySchema()),
    userController.updateUser
).swagger({
    tags: ["User Management"],
    summary: "Update User by ID",
    description: "Update user's basic information, role and status using uuid.",
    resDto: UserValidation.UpdateUserResponse(),
});

export default router;

Enter fullscreen mode Exit fullscreen mode

এখানে কোনো বাড়তি কনফিগারেশন ছাড়াই মিডলওয়্যার রিফ্লেকশনের কারণে ইঞ্জিনটি স্বয়ংক্রিয়ভাবে বুঝে নেয় যে :id একটি পাথ প্যারামিটার এবং রিকোয়েস্টের বডি হিসেবে updateUserBodySchema প্রত্যাশিত।

৪. রুট-লেভেল অপশন ও গ্লোবাল কনফিগ ওভাররাইড

যদি কোনো নির্দিষ্ট রুটে বিশেষ কনফিগারেশন প্রয়োজন হয়—যেমন ফাইল আপলোডের জন্য multipart/form-data, কাস্টম সাকসেস স্ট্যাটাস কোড (যেমন 201 Created), অথবা মিডলওয়্যারের বাইরে কাস্টম রিকোয়েস্ট স্কিমা ওভাররাইড করা—তবে .swagger({...})-এর ভেতর তা অত্যন্ত সহজে কাস্টমাইজ করা যায়:

router.patch(
    "/users/update/:id",
    upload.single("avatar"),
    userController.updateUser
).swagger({
    tags: ["User Management"],
    summary: "Update User with Avatar",
    description: "Update user details alongside multipart avatar upload.",
    contentType: "multipart/form-data", // 'application/json' | 'multipart/form-data'
    resContentType: "application/json",
    resDto: resDtoWithCode(UserValidation.UpdateUserResponse(), 201),
    requests: {
        params: z.object({
            id: z.string().uuid().openapi({ example: "550e8400-e29b-41d4-a716-446655440000" }),
        }),
        body: z.object({
            name: z.string().min(2).max(50),
            avatar: z.any().openapi({ type: "string", format: "binary" }),
        }),
    },
});

Enter fullscreen mode Exit fullscreen mode

এখানে contentType প্রোপার্টি ব্যবহার করে রিকোয়েস্টের কনটেন্ট টাইপ পরিবর্তন করা যায় এবং resDtoWithCode(schema, 201) ফাংশনের মাধ্যমে যেকোনো কাস্টম HTTP রেসপন্স স্ট্যাটাস কোড যুক্ত করা সম্ভব। এছাড়া requests অবজেক্টের মাধ্যমে যেকোনো রুট তার প্রয়োজনে গ্লোবাল প্যারামিটার বা বডি স্কিমাকে অনায়াসেই ওভাররাইড করতে পারে।

৬. Architectural Benchmark

মাপকাঠি swagger-jsdoc ম্যানুয়াল OpenAPI JSON NestJS / TSOA আমাদের কাস্টম ইঞ্জিন Source of Truth YAML কমেন্ট (আনটাইপড) আলাদা JSON ফাইল TypeScript Decorators Zod Schema টাইপ সেফটি শূন্য শূন্য উচ্চ সম্পূর্ণ এন্ড-টু-এন্ড সেফ রক্ষণাবেক্ষণ জটিলতা অত্যন্ত জটিল উচ্চ (মার্জ কনফ্লিক্ট প্রবণ) মাঝারি শূন্য (Zero-Boilerplate) রাউট সিঙ্ক্রোনাইজেশন ম্যানুয়াল ম্যানুয়াল ফ্রেমওয়ার্ক জেনারেটেড মিডলওয়্যার ড্রিভেন ফ্রেমওয়ার্ক কাপলিং কোনোটি নয় কোনোটি নয় ফ্রেমওয়ার্ক লক-ইন ভ্যানিলা Express.js

এই আর্কিটেকচারাল প্যাটার্ন ব্যবহারের ফলে এন্টারপ্রাইজ প্রজেক্টে এপিআই স্পেসিফিকেশন মেইনটেন্যান্স ওভারহেড প্রায় শূন্যে নেমে আসে। কোড রিফ্যাক্টরিংয়ের সময় ডকুমেন্টেশন ভেঙে যাওয়ার কোনো সুযোগ থাকে না, কারণ কোড এবং ডকস—দুটোই পরিচালিত হয় একই ভ্যালিডেশন স্কিমা দ্বারা।

원문에서 계속 ↗