Core API Methods

Suvidha uses CtxRequest which extends the standard Express.js Request object with a context property, for accessing request-specific data added by middlewares.

/**
 * Extends the standard Express.js `Request` object with a `context` property.
 * This allows you to store and access request-specific data throughout the
 * request lifecycle, particularly useful for middleware communication and
 * data sharing between middleware and request handler.
 *
 * @template C The type of the context object. Defaults to an empty object `{}`.
 *
 * @extends Express.js Request object
 */
interface CtxRequest<C extends Context = {}, ...> extends Request<...> {
    context: C;
}

The execution order of middlewares is the same as the order of declaration.

app.get(
    "/reports",
    suvidha()
        .query(pageSchema)
        .use(middyA)
        .use(middyB)
        .params(IdSchema)
        .handler((req) => {
            // Business logic
        }),
);

Execution order is query -> middyA -> middyB -> params -> handler.

Usage

Data Validation

Validate the request body using Zod schemas.

const userSchema = z.object({
    username: z.string().min(3),
    email: z.string().email(),
    age: z.number().min(13),
});

app.post(
    "/users",
    suvidha()
        .body(userSchema)
        .handler(async (req) => {
            const newUser = req.body; // type of newUser: z.infer<typeof UserSchema>
            // Execute business logic
        }),
);

Writing Middlewares

middlewares take CtxRequest and Response as input arguments and return Context | Promise<Context>.

// Middleware expects { user: User } in the context.
// specify the constraint in type signature, that way
// Suvidha will ensure that middleware is called
// only when expected context data is available.
type ExpectedContext = { user: User };

const middleware = (req: CtxRequest<ExpectedContext>, res: Response) => {
    // business logic
    return {
        // return context that will be merged with the current context
    };
};
app.get(
    "/reports",
    suvidha()
        .use(() => ({ user: authenticate() })) // { auth: Auth }
        .use(middleware) // { user: User }
        .handler((req) => {
            // Business logic
        }),
);

By specifying the constraint in type signature, we can build type-safe context through ordered middlewares - TypeScript enforces dependency sequence:

app.get(
    "/reports",
    suvidha()
        .use(() => ({ auth: getAuth() })) // { auth: Auth }
        .use((req) => ({
            // Requires previous auth
            user: getUser(req.context.auth), // + { user: User }
        }))
        .use(async (req) => ({
            // Requires user
            report: await fetchReport(req.context.user), // + { report: Report }
        }))
        .handler((req) => req.context.report), // Final: auth + user + report
);

Reorder middlewares → Type error ← Context dependencies broken.

Loosely Coupled Business Logic

Handles responses using onComplete and onErr when using Suvidha with Handlers. This separates your core business logic from framework-specific response methods, keeping your code cleaner by delegating response handling to the handlers.

const UserSchema = z.object({
    username: z.string().min(3),
    email: z.string().email(),
});

type UserDTO = z.infer<typeof UserSchema>;

// Keep your business logic clean and loosely coupled
const requestHandler = (user: UserDTO, role: string) => {
    // Execute business logic
    return {
        id: "67619c28758da37270b925a8",
    };
};

app.post(
    "/users",
    suvidha()
        .use(authenticate)
        .use(roleCheck)
        .body(UserSchema)
        .handler(async (req) => {
            const newUser = req.body;
            const { role } = req.context.user;
            return requestHandler(newUser, role); // Execute business logic
        }),
);

Suvidha as data validation middleware

To use Suvidha solely as data validation middleware with TypeScript inference, implement onSchemaErr method.

// Customize the error response
class CustomHandlers implements Handlers {
    onSchemaErr(_: ZodError, conn: Conn) {
        const fmt = {
            success: false,
            error: "VALIDATION_FAILURE",
        };
        conn.res.status(400).json(fmt);
    }

    onErr(): Promise<void> | void {
        throw new Error("Method not implemented.");
    }

    onComplete(): Promise<void> | void {
        throw new Error("Method not implemented.");
    }

    onPostResponse(): Promise<void> | void {
        throw new Error("Method not implemented.");
    }
}

const userSchema = z.object({
    username: z.string().min(3),
    email: z.string().email(),
});

// call the next middleware
const pass = (_: any, __: any, next: NextFunction) => void next();

app.post(
    "/users",
    suvidha()
        .body(userSchema)
        .handler((...args) => pass(...args)),
    (req, res) => {
        // type of req.body: z.infer<typeof userSchema>
        const { email, username } = req.body;
        res.send({ email, username });
    },
);

DRY Suvidha

If you have common middlewares across multiple routes, you can create a Suvidha instance and reuse it.

import { Suvidha, DefaultHandlers, Http } from "suvidha";

declare function authenticate(req: Request): Promise<{ role: string }>;

const suvidha = () => Suvidha.create(new DefaultHandlers());

// instance with middlewares for authentication
const auth = () =>
    suvidha().use(async (req) => {
        const user = await authenticate(req).catch((_) => {
            throw new Http.Unauthorized();
        });
        return { user };
    });

// instance with middlewares for admin authorization
const adminAuth = () =>
    // Reuse auth middleware
    auth().use((req) => {
        if (req.context.user.role !== "admin") {
            throw new Http.Forbidden();
        }
        return {};
    });

// Admin protected user creation endpoint
app.post(
    "/users",
    adminAuth()
        .body(userSchema)
        .handler(async (req) => {
            // Business logic
        }),
);

// Anyone authenticated user can access reports
app.get(
    "/reports",
    auth()
        .query(pageSchema)
        .handler(async (req) => {
            // Business logic
        }),
);

Adopting Suvidha in Existing Projects

Suvidha’s CtxRequest extends the standard Request object by adding a context property. Existing handlers that accept Request will also work with CtxRequest. The context can be accessed using CtxRequest.

declare function identity(req: Request): Promise<User>;

const BookSchema = z.object({
    title: z.string().min(3),
    author: z.string().min(3),
});

type Book = z.infer<typeof BookSchema>;
type User = { id: string; name: string };

function validateBody(req: Request, res: Response, zodSchema: ZodType<any>) {
    try {
        req.body = zodSchema.parse(req.body);
        next();
    } catch (err) {
        return void res.status(400).send("Bad Request");
    }
}

const authenticate = async (
    req: Request,
    res: Response,
    next: NextFunction,
) => {
    try {
        (req as any).user = await identity(req);
        next();
    } catch (_) {
        return void res.status(401).json({ error: "Unauthorized" });
    }
};

async function createBookHandler(
    req: Request<any, any, Book>,
    res: Response,
): Promise<void> {
    const user = (req as any).user as User;
    const book = req.body;
    // Business logic ...
}

app.post(
    "/books",
    authenticate,
    (req, res) => validateBody(req, res, BookSchema),
    createBookHandler,
);