🚀CRUD hoàn chỉnh với PostgreSQL, Express, Angular có sử dụng Docker🐳
Bài viết này sẽ hướng dẫn bạn xây dựng một ứng dụng CRUD hoàn chỉnh với PostgreSQL, Express, Angular bằng Typescript. Chúng ta sẽ đi qua từng bước chi tiết để tạo ra cấu trúc dự án và cài đặt các thành phần cần thiết. Trước tiên trong bài này các bạn hãy thực hiện theo từng bước để tạo được 1 project CRUD hoàn chỉnh. Bài viết sau mình sẽ đi phân tích từng thành phần cụ thể và từng kỹ thuật cụ thể được sử dụng trong bài này.
1. Tạo cấu trúc dự án
Trước tiên, hãy tạo một thư mục mới cho dự án:
mkdir my-crud-app
cd my-crud-app
mkdir backend frontend db
Trong một vài trường hợp ae dùng windown thì terminal cmd có lúc bị NGU một tý. Ae có thể chuyển sang dùng Git Bash Cmd
để sử dụng. Nếu ae đang sử dụng Vscode thì có thể làm như bên dưới để chuyển sang Git Bash
:
2. Cài đặt và khởi tạo Express
cd backend
npm init -y
npm install express body-parser cors pg dotenv typescript ts-node
npx tsc --init
npm i --save-dev @types/pg
npm i --save-dev @types/express
npm i --save-dev @types/cors
Sau đó, tạo các file và thư mục cần thiết cho backend:
mkdir src
cd src
mkdir controllers models routers services
touch index.ts
touch controllers/student.controller.ts
touch models/student.model.ts
touch routers/student.router.ts
touch services/student.service.ts
Hãy thêm script để run dev cho backend:
"scripts": {
"dev": "ts-node index.ts"
},
Khi đó file package.json
sẽ trông như thế này:
{
"name": "backend",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "ts-node src/index.ts"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"dotenv": "^16.0.3",
"express": "^4.18.2",
"pg": "^8.10.0",
"ts-node": "^10.9.1",
"typescript": "^5.0.4"
},
"devDependencies": {
"@types/cors": "^2.8.13",
"@types/express": "^4.17.17",
"@types/pg": "^8.6.6"
}
}
3. Khởi tạo Angular
cd ../..
ng new frontend --routing --style=css
cd frontend
touch src/app/student.ts
ng generate component students-list
ng generate component add-student
ng generate component edit-student
ng generate service student
Hãy thêm script để run dev cho backend:
"scripts": {
"dev": "ng serve --host=0.0.0.0 --watch --disable-host-check",
},
Khi đó file package.json
sẽ trông như thế này:
{
"name": "frontend",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"dev": "ng serve --host=0.0.0.0 --watch --disable-host-check",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test"
},
"private": true,
"dependencies": {
"@angular/animations": "^15.2.0",
"@angular/common": "^15.2.0",
"@angular/compiler": "^15.2.0",
"@angular/core": "^15.2.0",
"@angular/forms": "^15.2.0",
"@angular/platform-browser": "^15.2.0",
"@angular/platform-browser-dynamic": "^15.2.0",
"@angular/router": "^15.2.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.12.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^15.2.6",
"@angular/cli": "~15.2.6",
"@angular/compiler-cli": "^15.2.0",
"@types/jasmine": "~4.3.0",
"jasmine-core": "~4.5.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.0.0",
"typescript": "~4.9.4"
}
}
4. Tạo cấu trúc cho thư mục db
cd ../db
touch init.sql sample-data.sql
5. Tạo file docker-compose.yml
cd ..
touch docker-compose.yml
Bây giờ, chúng ta đã tạo xong cấu trúc dự án. Hãy bắt đầu cài đặt các thành phần cần thiết.
6. Cài đặt và cấu hình PostgreSQL
- Mở file
init.sql
trong thư mục db và thêm đoạn mã sau:
CREATE TABLE IF NOT EXISTS public.students (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
age INTEGER NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL
);
INSERT INTO public.students (name, age, email) VALUES
('Nguyen Van A', 20, 'nguyenvana@example.com'),
('Tran Thi B', 22, 'tranthib@example.com'),
('Pham Van C', 25, 'phamvanc@example.com');
7. Cài đặt và cấu hình Express
- Mở file
index.ts
trong thư mục src và thêm đoạn mã sau:
import express from 'express';
import bodyParser from 'body-parser';
import cors from 'cors';
import dotenv from 'dotenv';
import studentRouter from './routers/student.router';
dotenv.config();
const app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(cors());
app.use('/students', studentRouter);
app.listen(process.env.PORT, () => {
console.log(`Server is running on port ${process.env.PORT}`);
});
- Mở file
tsconfig.json
trong thư mục backend và thêm đoạn mã sau vào "compilerOptions
":
"esModuleInterop": true,
"moduleResolution": "node",
8. Cài đặt và cấu hình Angular
- Mở file
src/app/app.module.ts
trong thư mụcfrontend
và update như sau để import các module cần thiết:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { StudentsListComponent } from './students-list/students-list.component';
import { AddStudentComponent } from './add-student/add-student.component';
import { EditStudentComponent } from './edit-student/edit-student.component';
import { HttpClientModule } from '@angular/common/http';
import { FormsModule } from '@angular/forms';
@NgModule({
declarations: [
AppComponent,
StudentsListComponent,
AddStudentComponent,
EditStudentComponent,
],
imports: [BrowserModule, AppRoutingModule, HttpClientModule, FormsModule],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
9. Implement API CRUD cho backend
controllers/student.controller.ts
import { Request, Response } from "express";
import { StudentService } from "../services/student.service";
const studentService = new StudentService();
export class StudentController {
public async getAllStudents(req: Request, res: Response): Promise<void> {
try {
const students = await studentService.getAllStudents();
res.status(200).json(students);
} catch (error: any) {
res.status(500).json({ message: error.message });
}
}
public async getStudentById(req: Request, res: Response): Promise<void> {
const id = parseInt(req.params.id);
try {
const student = await studentService.getStudentById(id);
res.status(200).json(student);
} catch (error: any) {
res.status(500).json({ message: error.message });
}
}
public async createStudent(req: Request, res: Response): Promise<void> {
try {
const student = await studentService.createStudent(req.body);
res.status(201).json(student);
} catch (error: any) {
res.status(500).json({ message: error.message });
}
}
public async updateStudent(req: Request, res: Response): Promise<void> {
const id = parseInt(req.params.id);
try {
const student = await studentService.updateStudent(id, req.body);
res.status(200).json(student);
} catch (error: any) {
res.status(500).json({ message: error.message });
}
}
public async deleteStudent(req: Request, res: Response): Promise<void> {
const id = parseInt(req.params.id);
try {
await studentService.deleteStudent(id);
res.status(200).json({ message: "Student deleted successfully" });
} catch (error: any) {
res.status(500).json({ message: error.message });
}
}
}
models/student.model.ts
import { Pool } from "pg";
import dotenv from "dotenv";
dotenv.config();
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
export class StudentModel {
public async getAllStudents(): Promise<any[]> {
const query = "SELECT * FROM students ORDER BY id ASC";
const result = await pool.query(query);
return result.rows;
}
public async getStudentById(id: number): Promise<any> {
const query = "SELECT * FROM students WHERE id = $1";
const result = await pool.query(query, [id]);
return result.rows[0];
}
public async createStudent(student: any): Promise<any> {
const query = "INSERT INTO students (name, age, email) VALUES ($1, $2, $3) RETURNING *";
const values = [student.name, student.age, student.email];
const result = await pool.query(query, values);
return result.rows[0];
}
public async updateStudent(id: number, student: any): Promise<any> {
const query = "UPDATE students SET name = $1, age = $2, email = $3 WHERE id = $4 RETURNING *";
const values = [student.name, student.age, student.email, id];
const result = await pool.query(query, values);
return result.rows[0];
}
public async deleteStudent(id: number): Promise<void> {
const query = "DELETE FROM students WHERE id = $1";
await pool.query(query, [id]);
}
}
routers/student.router.ts
import { Router } from "express";
import { StudentController } from "../controllers/student.controller";
const studentController = new StudentController();
const router = Router();
router.get("/", studentController.getAllStudents);
router.get("/:id", studentController.getStudentById);
router.post("/", studentController.createStudent);
router.put("/:id", studentController.updateStudent);
router.delete("/:id", studentController.deleteStudent);
export default router;
services/student.service.ts
import { StudentModel } from "../models/student.model";
const studentModel = new StudentModel();
export class StudentService {
public async getAllStudents(): Promise<any[]> {
return await studentModel.getAllStudents();
}
public async getStudentById(id: number): Promise<any> {
return await studentModel.getStudentById(id);
}
public async createStudent(student: any): Promise<any> {
return await studentModel.createStudent(student);
}
public async updateStudent(id: number, student: any): Promise<any> {
return await studentModel.updateStudent(id, student);
}
public async deleteStudent(id: number): Promise<void> {
return await studentModel.deleteStudent(id);
}
}
Giờ đây, chúng ta đã hoàn thành việc implement API CRUD cho backend. Đảm bảo rằng bạn đã cập nhật mã nguồn cho các file này trong thư mục backend/src.
10. Implement feature CRUD cho frontend
Trong thư mục frontend/src/app
, hãy cập nhật mã nguồn cho các file sau:
students-list/students-list.component.html
<div class="container mt-5">
<h2>Students List</h2>
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Age</th>
<th>Email</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let student of students">
<td>{{ student.id }}</td>
<td>{{ student.name }}</td>
<td>{{ student.age }}</td>
<td>{{ student.email }}</td>
<td>
<button class="btn btn-primary" (click)="editStudent(student.id)">Edit</button>
<button class="btn btn-danger" (click)="deleteStudent(student.id)">Delete</button>
</td>
</tr>
</tbody>
</table>
<button class="btn btn-success" routerLink="/add">Add Student</button>
</div>
students-list/students-list.component.ts
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { Student } from '../student';
import { StudentService } from '../student.service';
@Component({
selector: 'app-students-list',
templateUrl: './students-list.component.html',
styleUrls: ['./students-list.component.css'],
})
export class StudentsListComponent implements OnInit {
students: Student[] = [];
constructor(private studentService: StudentService, private router: Router) {}
ngOnInit(): void {
this.getStudents();
}
private getStudents(): void {
this.studentService.getStudents().subscribe((students: Student[]) => {
this.students = students;
});
}
editStudent(id: number): void {
this.router.navigate(['/edit', id]);
}
deleteStudent(id: number): void {
this.studentService.deleteStudent(id).subscribe(() => {
this.getStudents();
});
}
}
add-student/add-student.component.html
<div class="container mt-5">
<h2>Add Student</h2>
<form (ngSubmit)="addStudent()">
<div class="form-group">
<label>Name</label>
<input type="text" class="form-control" [(ngModel)]="student.name" name="name" required>
</div>
<div class="form-group">
<label>Age</label>
<input type="number" class="form-control" [(ngModel)]="student.age" name="age" required>
</div>
<div class="form-group">
<label>Email</label>
<input type="email" class="form-control" [(ngModel)]="student.email" name="email" required>
</div>
<button type="submit" class="btn btn-success">Add</button>
<button type="button" class="btn btn-danger" (click)="cancel()">Cancel</button>
</form>
</div>
add-student/add-student.component.ts
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { Student } from '../student';
import { StudentService } from '../student.service';
@Component({
selector: 'app-add-student',
templateUrl: './add-student.component.html',
styleUrls: ['./add-student.component.css'],
})
export class AddStudentComponent implements OnInit {
student: Student = new Student();
constructor(private studentService: StudentService, private router: Router) {}
ngOnInit(): void {}
addStudent(): void {
this.studentService.addStudent(this.student).subscribe(() => {
this.router.navigate(['/']);
});
}
cancel(): void {
this.router.navigate(['/']);
}
}
edit-student/edit-student.component.html
<div class="container mt-5">
<h2>Edit Student</h2>
<form (ngSubmit)="updateStudent()">
<div class="form-group">
<label>Name</label>
<input type="text" class="form-control" [(ngModel)]="student.name" name="name" required>
</div>
<div class="form-group">
<label>Age</label>
<input type="number" class="form-control" [(ngModel)]="student.age" name="age" required>
</div>
<div class="form-group">
<label>Email</label>
<input type="email" class="form-control" [(ngModel)]="student.email" name="email" required>
</div>
<button type="submit" class="btn btn-success">Update</button>
<button type="button" class="btn btn-danger" (click)="cancel()">Cancel</button>
</form>
</div>
edit-student/edit-student.component.ts
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Student } from '../student';
import { StudentService } from '../student.service';
@Component({
selector: 'app-edit-student',
templateUrl: './edit-student.component.html',
styleUrls: ['./edit-student.component.css'],
})
export class EditStudentComponent implements OnInit {
student: Student = new Student();
id: any;
constructor(
private studentService: StudentService,
private router: Router,
private route: ActivatedRoute
) {}
ngOnInit(): void {
this.id = parseInt(this.route.snapshot.paramMap.get('id') as any);
this.getStudent(this.id);
}
getStudent(id: number): void {
this.studentService.getStudent(id).subscribe((student: Student) => {
this.student = student;
});
}
updateStudent(): void {
this.studentService.updateStudent(this.id, this.student).subscribe(() => {
this.router.navigate(['/']);
});
}
cancel(): void {
this.router.navigate(['/']);
}
}
student.ts:
export class Student {
id: any;
name: any;
age: any;
email: any;
}
student.service.ts
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { Student } from './student';
@Injectable({
providedIn: 'root',
})
export class StudentService {
private baseURL = 'http://localhost:10001/students';
constructor(private httpClient: HttpClient) {}
getStudents(): Observable<Student[]> {
return this.httpClient.get<Student[]>(this.baseURL);
}
getStudent(id: number): Observable<Student> {
return this.httpClient.get<Student>(`${this.baseURL}/${id}`);
}
addStudent(student: Student): Observable<Object> {
return this.httpClient.post(`${this.baseURL}`, student);
}
updateStudent(id: number, student: Student): Observable<Object> {
return this.httpClient.put(`${this.baseURL}/${id}`, student);
}
deleteStudent(id: number): Observable<Object> {
return this.httpClient.delete(`${this.baseURL}/${id}`);
}
}
app.component.html
Xóa tất cả nội dung hiện tại và thay thế bằng đoạn mã sau:
<h1>DEMO CRUD APP</h1>
<router-outlet></router-outlet>
app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AddStudentComponent } from './add-student/add-student.component';
import { EditStudentComponent } from './edit-student/edit-student.component';
import { StudentsListComponent } from './students-list/students-list.component';
const routes: Routes = [
{ path: '', component: StudentsListComponent },
{ path: 'add', component: AddStudentComponent },
{ path: 'edit/:id', component: EditStudentComponent },
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}
Cập nhật link Bootstrap để cho giao diện đẹp hơn bằng cách thêm đoạn sau vào file src/index.html
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
<!-- jQuery library -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<!-- Latest compiled JavaScript -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>
-> lúc này file src/index.html
sẽ trông như sau:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>CRUD APP</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
<!-- jQuery library -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<!-- Latest compiled JavaScript -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>
</head>
<body>
<app-root></app-root>
</body>
</html>
11. Cấu hình docker
Hãy tạo hai tệp Dockerfile cho cả backend
và frontend
và thêm mã nguồn tương ứng.
- Backend: Tạo một tệp mới tên là
Dockerfile
trong thư mục backend và thêm đoạn mã sau:
cd ../backend
touch Dockerfile
FROM node:14
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 10001
CMD [ "npm", "run", "dev" ]
- Frontend: Tạo một tệp mới tên là
Dockerfile
trong thư mục frontend và thêm đoạn mã sau:
cd ../frontend
touch Dockerfile
FROM node:14
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 10002
CMD [ "npm", "run", "dev" ]
- Mở file
docker-compose.yml
và thêm đoạn mã sau:
version: '3.8'
services:
backend:
build: ./backend
volumes:
- ./backend:/app
- /app/node_modules
ports:
- '10001:10001'
environment:
- DATABASE_URL=postgres://username:password@db:5432/dbname
- PORT=10001
depends_on:
- db
frontend:
build: ./frontend
volumes:
- ./frontend:/app
- /app/node_modules
ports:
- '10002:4200'
depends_on:
- backend
db:
image: postgres:12-alpine
volumes:
- db-data:/var/lib/postgresql/data
- ./db/init.sql:/docker-entrypoint-initdb.d/init.sql
ports:
- '10003:5432'
environment:
- POSTGRES_USER=username
- POSTGRES_PASSWORD=password
- POSTGRES_DB=dbname
volumes:
db-data:
RUN APP
Sau khi thực hiện các bước trên, bạn có thể chạy lệnh docker-compose up -d
trong thư mục gốc của dự án. Nếu ứng dụng hoạt động chính xác, bạn sẽ thấy giao diện CRUD hoạt động trên cổng 10002 của máy chủ.
Kết quả thu được sẽ như vầy:
Mình hy vọng bạn thích bài viết này và học thêm được điều gì đó mới.
Donate mình một ly cafe hoặc 1 cây bút bi để mình có thêm động lực cho ra nhiều bài viết hay và chất lượng hơn trong tương lai nhé. À mà nếu bạn có bất kỳ câu hỏi nào thì đừng ngại comment hoặc liên hệ mình qua: Zalo - 0374226770 hoặc Facebook. Mình xin cảm ơn.
Momo: NGUYỄN ANH TUẤN - 0374226770
TPBank: NGUYỄN ANH TUẤN - 0374226770 (hoặc 01681423001)
All rights reserved