1. Introducing the real-life project
In the next 3 lessons, we will build Task Management App Complete with Vibe Coding. The goal is to experience the end-to-end development process with AI.
Project overview:
| Layer | Technology | Lesson |
|---|---|---|
| Backend | Node.js + Express + TypeScript | Lesson 14 (this lesson) |
| Frontend | React + Next.js + TailwindCSS | Lesson 15 |
| Mobile | React Native / Flutter | Lesson 16 |
| Database | PostgreSQL + Prisma ORM | Lesson 14 |
| Auth | JWT + bcrypt | Lesson 14 |
2. Create a project using Vibe Coding
Step 1: Use Agent Mode to scaffold
// Prompt cho Agent Mode:
Create a new Node.js backend project with:
- TypeScript configuration
- Express.js server
- Prisma ORM with PostgreSQL
- JWT authentication
- Project structure following clean architecture
- ESLint + Prettier configuration
- Docker Compose for PostgreSQL
Agent Mode will automatically:
- Run
npm init, install dependencies - Create
tsconfig.json,.eslintrc,.prettierrc - Create folder structure
- Setup Docker Compose
Step 2: Project structure is created
task-api/
├── src/
│ ├── controllers/
│ │ ├── auth.controller.ts
│ │ └── task.controller.ts
│ ├── middleware/
│ │ ├── auth.middleware.ts
│ │ └── validate.middleware.ts
│ ├── routes/
│ │ ├── auth.routes.ts
│ │ └── task.routes.ts
│ ├── services/
│ │ ├── auth.service.ts
│ │ └── task.service.ts
│ ├── types/
│ │ └── index.ts
│ └── app.ts
├── prisma/
│ └── schema.prisma
├── docker-compose.yml
├── tsconfig.json
└── package.json
3. Database Schema Design with AI
// Prompt:
Design a Prisma schema for a task management app with:
- Users (email, password hash, name, avatar)
- Projects (name, description, owner)
- Tasks (title, description, status, priority, assignee, due date)
- Comments on tasks
- Project memberships with roles
Copilot will generate schema:
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(uuid())
email String @unique
password String
name String
avatar String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
ownedProjects Project[]
memberships ProjectMember[]
assignedTasks Task[] @relation("AssignedTasks")
createdTasks Task[] @relation("CreatedTasks")
comments Comment[]
}
model Project {
id String @id @default(uuid())
name String
description String?
ownerId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
owner User @relation(fields: [ownerId], references: [id])
members ProjectMember[]
tasks Task[]
}
model ProjectMember {
id String @id @default(uuid())
role Role @default(MEMBER)
userId String
projectId String
user User @relation(fields: [userId], references: [id])
project Project @relation(fields: [projectId], references: [id])
@@unique([userId, projectId])
}
enum Role {
OWNER
ADMIN
MEMBER
}
model Task {
id String @id @default(uuid())
title String
description String?
status TaskStatus @default(TODO)
priority Priority @default(MEDIUM)
dueDate DateTime?
projectId String
assigneeId String?
creatorId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
project Project @relation(fields: [projectId], references: [id])
assignee User? @relation("AssignedTasks", fields: [assigneeId], references: [id])
creator User @relation("CreatedTasks", fields: [creatorId], references: [id])
comments Comment[]
}
enum TaskStatus {
TODO
IN_PROGRESS
IN_REVIEW
DONE
}
enum Priority {
LOW
MEDIUM
HIGH
URGENT
}
model Comment {
id String @id @default(uuid())
content String
taskId String
authorId String
createdAt DateTime @default(now())
task Task @relation(fields: [taskId], references: [id])
author User @relation(fields: [authorId], references: [id])
}
4. Authentication — JWT Flow
// Prompt:
Implement JWT authentication with:
- Register endpoint with email validation and password hashing
- Login endpoint returning access + refresh tokens
- Auth middleware that validates JWT
- Refresh token rotation
- Use bcrypt for password hashing
Copilot generate auth service:
// src/services/auth.service.ts
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import { prisma } from '../lib/prisma';
export class AuthService {
async register(email: string, password: string, name: string) {
const existingUser = await prisma.user.findUnique({ where: { email } });
if (existingUser) {
throw new Error('Email already registered');
}
const hashedPassword = await bcrypt.hash(password, 12);
const user = await prisma.user.create({
data: { email, password: hashedPassword, name },
});
return this.generateTokens(user.id);
}
async login(email: string, password: string) {
const user = await prisma.user.findUnique({ where: { email } });
if (!user || !(await bcrypt.compare(password, user.password))) {
throw new Error('Invalid credentials');
}
return this.generateTokens(user.id);
}
private generateTokens(userId: string) {
const accessToken = jwt.sign(
{ userId },
process.env.JWT_SECRET!,
{ expiresIn: '15m' }
);
const refreshToken = jwt.sign(
{ userId },
process.env.JWT_REFRESH_SECRET!,
{ expiresIn: '7d' }
);
return { accessToken, refreshToken };
}
}
5. REST API — CRUD Operations
// Prompt:
Create CRUD endpoints for tasks with:
- GET /api/tasks — list tasks with filtering and pagination
- POST /api/tasks — create new task
- PUT /api/tasks/:id — update task
- DELETE /api/tasks/:id — delete task
- Include authorization checks
- Validate request body with Zod
Copilot generate controller:
// src/controllers/task.controller.ts
import { Request, Response } from 'express';
import { z } from 'zod';
import { prisma } from '../lib/prisma';
const createTaskSchema = z.object({
title: z.string().min(1).max(255),
description: z.string().optional(),
priority: z.enum(['LOW', 'MEDIUM', 'HIGH', 'URGENT']).default('MEDIUM'),
dueDate: z.string().datetime().optional(),
projectId: z.string().uuid(),
assigneeId: z.string().uuid().optional(),
});
export class TaskController {
async list(req: Request, res: Response) {
const { projectId, status, priority, page = '1', limit = '20' } = req.query;
const tasks = await prisma.task.findMany({
where: {
projectId: projectId as string,
status: status as any,
priority: priority as any,
},
include: {
assignee: { select: { id: true, name: true, avatar: true } },
_count: { select: { comments: true } },
},
skip: (Number(page) - 1) * Number(limit),
take: Number(limit),
orderBy: { createdAt: 'desc' },
});
res.json({ data: tasks });
}
async create(req: Request, res: Response) {
const data = createTaskSchema.parse(req.body);
const task = await prisma.task.create({
data: {
...data,
dueDate: data.dueDate ? new Date(data.dueDate) : null,
creatorId: req.userId!, // from auth middleware
},
});
res.status(201).json({ data: task });
}
}
6. Vibe Coding Workflow for Backend
Optimal process:
┌──────────────────────────────────────────────────┐
│ BACKEND VIBE CODING WORKFLOW │
├──────────────────────────────────────────────────┤
│ │
│ 1. Mô tả feature bằng ngôn ngữ tự nhiên │
│ ↓ │
│ 2. Agent Mode scaffold files │
│ ↓ │
│ 3. Review generated code │
│ ↓ │
│ 4. Prompt: "Write tests for this" │
│ ↓ │
│ 5. Agent chạy tests, fix lỗi │
│ ↓ │
│ 6. Prompt: "Add error handling and validation" │
│ ↓ │
│ 7. Review final code → Commit │
│ │
└──────────────────────────────────────────────────┘
Tips when Vibe Coding backend:
- Clearly describe business logic: AI does not know your domain
- Request tests at the same time: "Create the service AND its unit tests"
- Review security: Always check input validation, auth checks
- Database migrations: Use
prisma migrate devafter AI changed the schema - Use copilot-instructions.md: Define coding standards for the project
7. Testing with AI
// Prompt:
Write comprehensive tests for AuthService including:
- Successful registration
- Duplicate email registration
- Successful login
- Invalid credentials
- Token generation and verification
Use Jest with supertest for integration tests.
// src/services/__tests__/auth.service.test.ts
import { AuthService } from '../auth.service';
import { prisma } from '../../lib/prisma';
import bcrypt from 'bcrypt';
describe('AuthService', () => {
const authService = new AuthService();
beforeEach(async () => {
await prisma.user.deleteMany();
});
describe('register', () => {
it('should create user and return tokens', async () => {
const result = await authService.register(
'[email protected]', 'Password123!', 'Test User'
);
expect(result.accessToken).toBeDefined();
expect(result.refreshToken).toBeDefined();
const user = await prisma.user.findUnique({
where: { email: '[email protected]' },
});
expect(user).toBeDefined();
expect(user!.name).toBe('Test User');
});
it('should hash password correctly', async () => {
await authService.register(
'[email protected]', 'Password123!', 'Test User'
);
const user = await prisma.user.findUnique({
where: { email: '[email protected]' },
});
expect(user!.password).not.toBe('Password123!');
expect(await bcrypt.compare('Password123!', user!.password)).toBe(true);
});
it('should reject duplicate email', async () => {
await authService.register(
'[email protected]', 'Password123!', 'Test User'
);
await expect(
authService.register('[email protected]', 'Other456!', 'Other User')
).rejects.toThrow('Email already registered');
});
});
});
8. Error Handling Pattern
// Prompt:
Add global error handling middleware with:
- Custom AppError class with status codes
- Async error wrapper for controllers
- Prisma error handling (unique constraint, not found)
- Request validation errors (Zod)
- Structured error response format
9. Summary
| Step | Vibe Coding Approach | Time saving |
|---|---|---|
| Project setup | Agent Mode scaffold | ~2 hours → 5 minutes |
| DB Schema | Describing entities → Prisma schema | ~1 hour → 10 minutes |
| Auth | Describe flow → full implementation | ~4 hours → 20 minutes |
| CRUD API | Describe endpoints → controller + service | ~3 hours → 15 minutes |
| Tests | "Write tests for X" → comprehensive suite | ~3 hours → 10 minutes |
Important note: The above time savings are ideal. Actually you still need it review, debug, and refine AI code is born. Vibe Coding reduces coding time but Do not reduce review time.
Next article: Construction Frontend for app — React + Next.js + TailwindCSS, API connection, responsive design.