Tutorial
Build a Task Manager API
An end-to-end walkthrough: scaffold a project, generate a Task module, add custom middleware, run the guarded build, and deploy with Docker.
Step 1 — Scaffold
bash
npx create-express-modular taskflow-apiPick: Mongoose, Zod, JWT auth with cookies, Docker = Yes.
Step 2 — Boot the dev server
bash
cd taskflow-api
npm install
cem devOpen http://localhost:5000 for the branded welcome page.
Step 3 — Add an env variable
bash
cem add env RESEND_API_KEYUpdated in .env, .env.example and your typed config — all in one command.
Step 4 — Generate the Task module
bash
cem add module TaskAnswer Yes to constants, No to utils.
4.1 — Interface
src/app/modules/Task/task.interface.tsts
import { Types } from 'mongoose';
export type TTaskStatus = 'TODO' | 'IN_PROGRESS' | 'DONE';
export interface ITask {
title: string;
description?: string;
status: TTaskStatus;
dueDate?: Date;
assignedUser: Types.ObjectId;
}4.2 — Constants
src/app/modules/Task/task.constant.tsts
export const TASK_STATUS = {
TODO: 'TODO',
IN_PROGRESS: 'IN_PROGRESS',
DONE: 'DONE',
} as const;
export const taskSearchableFields = ['title', 'description'];4.3 — Model
src/app/modules/Task/task.model.tsts
import { Schema, model } from 'mongoose';
import { ITask } from './task.interface';
import { TASK_STATUS } from './task.constant';
const taskSchema = new Schema<ITask>(
{
title: { type: String, required: true, trim: true },
description: { type: String },
status: {
type: String,
enum: Object.values(TASK_STATUS),
default: 'TODO',
},
dueDate: { type: Date },
assignedUser: { type: Schema.Types.ObjectId, ref: 'User', required: true },
},
{ timestamps: true },
);
export const Task = model<ITask>('Task', taskSchema);4.4 — Validation
src/app/modules/Task/task.validation.tsts
import { z } from 'zod';
import { TASK_STATUS } from './task.constant';
const createTaskValidationSchema = z.object({
body: z.object({
title: z.string({ required_error: 'Title is required' }).min(3).max(100),
description: z.string().optional(),
status: z.nativeEnum(TASK_STATUS).default('TODO'),
dueDate: z.string().datetime().optional()
.transform((v) => (v ? new Date(v) : undefined)),
}),
});
const updateTaskValidationSchema = z.object({
body: z.object({
title: z.string().min(3).max(100).optional(),
description: z.string().optional(),
status: z.nativeEnum(TASK_STATUS).optional(),
dueDate: z.string().datetime().optional()
.transform((v) => (v ? new Date(v) : undefined)),
}),
});
export const TaskValidation = {
createTaskValidationSchema,
updateTaskValidationSchema,
};4.5 — Service (with QueryBuilder)
src/app/modules/Task/task.service.tsts
import { ITask } from './task.interface';
import { Task } from './task.model';
import QueryBuilder from '../../utils/QueryBuilder';
import { taskSearchableFields } from './task.constant';
const createTaskIntoDB = async (payload: ITask) => Task.create(payload);
const getAllTasksFromDB = async (query: Record<string, unknown>) => {
const taskQuery = new QueryBuilder(
Task.find().populate('assignedUser', '-password'),
query,
)
.search(taskSearchableFields)
.filter()
.sort()
.paginate()
.fields();
const result = await taskQuery.modelQuery;
const meta = await taskQuery.countTotal();
return { meta, result };
};
const getSingleTaskFromDB = (id: string) =>
Task.findById(id).populate('assignedUser', '-password');
const updateTaskInDB = (id: string, payload: Partial<ITask>) =>
Task.findByIdAndUpdate(id, payload, { new: true, runValidators: true });
const deleteTaskFromDB = (id: string) => Task.findByIdAndDelete(id);
export const TaskServices = {
createTaskIntoDB,
getAllTasksFromDB,
getSingleTaskFromDB,
updateTaskInDB,
deleteTaskFromDB,
};4.6 — Controller
src/app/modules/Task/task.controller.tsts
import { Request, Response } from 'express';
import { StatusCodes } from 'http-status-codes';
import { catchAsync } from '../../utils/catchAsync';
import sendResponse from '../../utils/sendResponse';
import { TaskServices } from './task.service';
const createTask = catchAsync(async (req: Request, res: Response) => {
const user = req.user;
const result = await TaskServices.createTaskIntoDB({
...req.body,
assignedUser: user._id,
});
sendResponse(res, {
statusCode: StatusCodes.CREATED,
success: true,
message: 'Task created successfully',
data: result,
});
});
const getAllTasks = catchAsync(async (req, res) => {
const { meta, result } = await TaskServices.getAllTasksFromDB(req.query);
sendResponse(res, {
statusCode: StatusCodes.OK,
success: true,
message: 'Tasks retrieved successfully',
meta,
data: result,
});
});
// getSingleTask, updateTask, deleteTask follow the same pattern.
export const TaskControllers = { createTask, getAllTasks /* ... */ };4.7 — Route
src/app/modules/Task/task.route.tsts
import express from 'express';
import { TaskControllers } from './task.controller';
import { TaskValidation } from './task.validation';
import validateRequest from '../../utils/validateRequest';
import auth from '../../middlewares/auth.middleware';
const router = express.Router();
router.get('/', auth('USER', 'ADMIN'), TaskControllers.getAllTasks);
router.get('/:id', auth('USER', 'ADMIN'), TaskControllers.getSingleTask);
router.post(
'/',
auth('USER'),
validateRequest(TaskValidation.createTaskValidationSchema),
TaskControllers.createTask,
);
router.patch(
'/:id',
auth('USER', 'ADMIN'),
validateRequest(TaskValidation.updateTaskValidationSchema),
TaskControllers.updateTask,
);
router.delete('/:id', auth('ADMIN'), TaskControllers.deleteTask);
export const TaskRoutes = router;Step 5 — Add a custom middleware
bash
cem add middleware requestLoggersrc/app/middlewares/requestLogger.middleware.tsts
import { NextFunction, Request, Response } from 'express';
import { catchAsync } from '../utils/catchAsync';
import logger from '../utils/logger';
const requestLogger = catchAsync(
async (req: Request, _res: Response, next: NextFunction) => {
logger.info(`Incoming Request ➔ [${req.method}] ${req.originalUrl}`);
next();
},
);
export default requestLogger;Register it globally in src/app.ts:
ts
app.use(requestLogger);Step 6 — Inspect the project
bash
cem listStep 7 — Guarded build
bash
cem buildRuns the middleware naming guard, the architecture guard, then tsc. If any guard fails, you get a precise, actionable error.
Step 8 — Quality check
bash
cem checkStep 9 — Ship with Docker
bash
docker compose up --build