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-api

Pick: Mongoose, Zod, JWT auth with cookies, Docker = Yes.

Step 2 — Boot the dev server

bash
cd taskflow-api
npm install
cem dev

Open http://localhost:5000 for the branded welcome page.

Step 3 — Add an env variable

bash
cem add env RESEND_API_KEY

Updated in .env, .env.example and your typed config — all in one command.

Step 4 — Generate the Task module

bash
cem add module Task

Answer 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 requestLogger
src/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 list

Step 7 — Guarded build

bash
cem build

Runs 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 check

Step 9 — Ship with Docker

bash
docker compose up --build