Get Started
Usage
Reference
API
Guide to Suvidha’s APIs
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;
}
Main class for creating a new instance of Suvidha; requires a Handlers instance.
constructor(handlers: Handlers);
Refer to Handlers for more details.
Purpose
Validates and types the request body using Zod schemas.
Parameters
schema
: Zod schema definition.- Returns: Chainable Suvidha instance.
Example
suvidha().body(
z.object({
email: z.string().email(),
password: z.string().min(8),
}),
);
Behavior
- Validates request body before handler execution.
- Overrides default
any
type with schema inference.
Purpose
Builds request context through chainable middleware. Each call merges new properties into the context while preserving type safety.
Any exceptions thrown will be handled by onErr
handler.
- Order-Sensitive: Middleware executes in the order of declaration.
- Type Accumulation: Each middleware’s return type merges with previous context.
- Immutable Context: Each middleware gets current context in
Readonly
wrapper to prevent mutations. - Early Termination: Middleware can end response with
res.send()
.
...
spread operator, new_ctx = { ...current_ctx, ...ctx }
Middleware Signature
type Middleware<Ctx> = (
req: CtxRequest<CurrentContext>,
res: Response,
) => Ctx | Promise<Ctx>;
Example
.use(async (req) => ({
user: await authenticate(req)
}))
.use((req) => ({
permissions: getPermissions(req.context.user)
}))
Type Safety
// In handler:
req.context.user; // AuthenticatedUser
req.context.permissions; // string[]
Purpose
Defines the final request handler with full type safety.
Signature
.handler(
(req: CtxRequest, res: Response, next: NextFunction) => Reply
)
Parameters
req
is express’Request
object with added context.res
is express’Request
object.next
is express’NextFunction
to call next middleware.
Acts as an Express.js middleware. It executes the configured Suvidha middleware functions (both validation via .body()
, .query()
, .params()
and custom logic via .use()
) in the order they are defined. After successful execution of these middlewares, .next()
calls the next middleware in the Express.js chain using the express’ next()
function.
It’s like handler()
but with request handler as express’ next()
function.
Example
const bannedHosts = new Set(["malicious.example.com", "spam.org"]);
app.use(
"/protected",
suvidha()
.use(async (req) => {
const clientHost = req.hostname;
if (bannedHosts.has(clientHost)) {
throw Http.Forbidden.body({
message: `Host "${clientHost}" is banned.`,
});
}
return {};
})
.query(z.object({ resourceId: z.string() }))
.next(), // Proceed to the protected resource handler
(req, res) => {
// Access validated query parameter
const resourceId = req.query.resourceId; // infered type: string
res.json({ message: `Access granted to resource: ${resourceId}` });
},
);
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
}),
);
app.get(
"/reports",
suvidha()
.use(() => ({ user: authenticate() })) // { auth: Auth }
.use(middleware) // { user: User }
.handler((req) => {
// Business logic
}),
);
app.get(
"/reports",
suvidha()
.use(middleware) // error: middleware expects { user: User } in context
.use(() => ({ user: authenticate() })) // { auth: Auth }
.handler((req) => {
// Business logic
}),
);
TypeScript will throw an error because middleware
expects { user: User }
in context,
but the context is {}
because the authenticate
middleware has not been called yet.
Property 'user' is missing in type '{}' but required in type 'ExpectedContext'.
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(),
});
const verifyBody = <T extends z.ZodTypeAny>(schema: T) =>
suvidha().body(schema).next();
app.post("/users", verifyBody(userSchema), (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,
);