Skip to main content

Controller

Pashmak can accept both function and class controllers.

Functional Controllers

Functional controllers are simple async functions that receive request and response objects:

import { Request, Response } from "@devbro/pashmak/router";
import { router } from "@devbro/pashmak/facades";

router().addRoute(
["GET"],
"/api/v1/hello",
async (req: Request, res: Response) => {
return { message: "Hello World" };
},
);

Class Controllers

Class controllers provide a cleaner way to organize your routes using decorators:

import { db, storage, logger } from "@devbro/pashmak/facades";
import { ctx } from "@devbro/pashmak/context";
import {
Request,
Response,
Model,
Param,
BaseController,
Controller,
Get,
Post,
Put,
Delete,
} from "@devbro/pashmak/router";

@Controller("/api/v1/cats", {
middlewares: [mid1, mid2],
})
export class CatController extends BaseController {
@Get("/", { middlewares: [logResponseMiddleware] })
async list() {
const r = await db().runQuery({
sql: "select * from cats",
parts: [],
bindings: [],
});
return {
message: "GET cats",
data: r,
};
}

@Post("/")
async store() {
const req = ctx().get<Request>("request");
logger().info({ msg: "request details", body: req.body, files: req.files });

return { success: true };
}

@Get("/:id")
async show(@Param("id") id: string) {
return { id, name: "cat name" };
}

@Put("/:id")
async update(
@Param("id") id: string,
@Model(CatModel, "id", "id") cat: CatModel,
) {
// Model decorator automatically fetches the cat by id
cat.name = "Updated name";
await cat.save();
return cat;
}

@Delete("/:id")
async delete(@Param("id") id: string) {
await CatModel.deleteById(id);
return { success: true };
}

@Get("/file")
async getFile() {
const res = ctx().get<Response>("response");
await res.writeHead(200, {
"Content-Type": "image/jpeg",
});

(await storage().getStream("test.jpg")).pipe(res);
}

@Get("/file-details")
async getFileDetails() {
return await storage().metadata("test.jpg");
}

@Get("/:id/notes/:noteId")
showNotes(@Param("noteId") noteId: string, @Param("id") id: string) {
return { id, noteId, notes: [] };
}
}

Controller Decorators

HTTP Method Decorators

  • @Get("path", options?) - Handle GET and HEAD requests
  • @Post("path", options?) - Handle POST requests
  • @Put("path", options?) - Handle PUT requests
  • @Delete("path", options?) - Handle DELETE requests
  • @Patch("path", options?) - Handle PATCH requests

Each decorator accepts an optional configuration object:

@Get("/path", {
middlewares: [middleware1, middleware2] // Route-specific middlewares
})

Parameter Decorators

@Param(param_name)

Extracts a specific parameter from the request URL and injects it into the controller method:

@Get("/:id")
async show(@Param("id") id: string) {
// id contains the value from the URL parameter
return { id };
}

@Model(ModelClass, param_name?, model_field?)

Automatically fetches a model instance based on a route parameter and injects it into the controller method:

  • ModelClass - The ORM model class to fetch
  • param_name - Optional, defaults to "id". The parameter name in the URL.
  • model_field - Optional, defaults to "id". The field in the model to match against the param value.
@Put("/:userId")
async update(@Param("userId") userId: string, @Model(User,'userId','id') user: User) {
// user is automatically fetched from database
user.name = "Updated name";
await user.save();
return user;
}

@ValidatedRequest(validation_schema)

This decorator is defined under src/helpers/validation.ts to allow modification/replacement of main validation library. Currently there is support for both Yup and Zod validation libraries.

import * as yup from "yup";
import zod from "zod";

const createUserSchema = yup.object({
name: yup.string().required(),
email: yup.string().email().required(),
});

const updateUserSchema = zod.object({
name: zod.string().optional(),
email: zod.string().email().optional(),
});

@Post()
async create(@ValidatedRequest(createUserSchema) data: any) {
// data is validated and type-safe
return await User.create(data);
}

@Put("/:id")
async update(@Param("id") id: string, @ValidatedRequest(updateUserSchema) data: any) {
return { "message": "User updated", data};
}

Registering Controllers

To use a controller, it must be registered with the router:

import { router } from "@devbro/pashmak/facades";
import { CatController } from "./app/controllers/CatController";

router().addController(CatController);

Direct Response Manipulation

For more complex responses, you can directly modify the Response object:

import { Request, Response } from "@devbro/pashmak/router";

async (req: Request, res: Response) => {
res.writeHead(200, { "Content-Type": "application/json" });
res.write(JSON.stringify({ message: "Custom response" }));
};

If do so, make sure to call res.writeHead() to set the status code and headers before writing the response body. When writing response directly, it is highly recommended to use res.write() instead of res.end() to allow middlewares and error handling to function properly.

Decorators

Currently decorators are supported only on classes and class-methods.

making your own decorators

to simplify making your own decorators you can use createParamDecorator helper from @devbro/pashmak/router

import * as yup from "yup";
import { z } from "zod";
import { ctx } from "@devbro/pashmak/context";
import { Request, createParamDecorator } from "@devbro/pashmak/router";

export function ValidatedRequest(
validationRules:
| yup.ObjectSchema<any>
| (() => yup.ObjectSchema<any>)
| z.ZodType<any>
| (() => z.ZodType<any>),
): ParameterDecorator {
return createParamDecorator(async () => {
const schema =
typeof validationRules === "function"
? validationRules()
: validationRules;
const requestBody = ctx().get<Request>("request").body;

// Check if it's a Zod schema by checking for parse method
if ("parse" in schema && typeof schema.parse === "function") {
return await schema.parseAsync(requestBody);
}

// Otherwise, treat it as Yup schema
const rc = await (schema as yup.ObjectSchema<any>)
.noUnknown()
.validate(requestBody, { abortEarly: false });

return rc;
});
}