- vừa được xem lúc

Blog#241: 🚀CRUD hoàn chỉnh với PostgreSQL, Express, Angular có sử dụng Docker🐳

0 0 21

Người đăng: NGUYỄN ANH TUẤN

Theo Viblo Asia

241

Hi, I'm Tuan, a Full-stack Web Developer from Tokyo 😊. Follow my blog to not miss out on useful and interesting articles in the future.

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: image.png

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ục frontend 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ả backendfrontend 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:

image.png

And Finally

As always, I hope you enjoyed this article and got something new. Thank you and see you in the next articles!

If you liked this article, please give me a like and subscribe to support me. Thank you. 😊

Ref

Bình luận

Bài viết tương tự

- vừa được xem lúc

Cài đặt WSL / WSL2 trên Windows 10 để code như trên Ubuntu

Sau vài ba năm mình chuyển qua code trên Ubuntu thì thật không thể phủ nhận rằng mình đã yêu em nó. Cá nhân mình sử dụng Ubuntu để code web thì thật là tuyệt vời.

0 0 376

- vừa được xem lúc

Phần 1: Giới thiệu về Kubernetes

Kubernetes là gì. Trang chủ: https://kubernetes.io/. Ai cần Kubernetes.

0 0 80

- vừa được xem lúc

Docker: Chưa biết gì đến biết dùng (Phần 1- Lịch sử)

1. Vì sao nên sử dụng. . .

0 0 89

- vừa được xem lúc

Docker - những kiến thức cơ bản phần 1

Giới thiệu. Nếu bạn đang làm ở một công ty công nghệ thông tin, chắc rằng bạn đã được nghe nói về Docker.

0 0 65

- vừa được xem lúc

Docker: Chưa biết gì đến biết dùng (Phần 2 - Dockerfile)

1. Mở đầu.

0 0 53

- vừa được xem lúc

Docker: Chưa biết gì đến biết dùng (Phần 3: Docker-compose)

1. Mở đầu. . .

0 0 106