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

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 bài 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

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ả 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ủ.

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. 😊

NGUYỄN ANH TUẤN

Xin chào, mình là Tuấn, một kỹ sư phần mềm đang làm việc tại Tokyo. Đây là blog cá nhân nơi mình chia sẻ kiến thức và kinh nghiệm trong quá trình phát triển bản thân. Hy vọng blog sẽ là nguồn cảm hứng và động lực cho các bạn. Hãy cùng mình học hỏi và trưởng thành mỗi ngày nhé!

Đăng nhận xét

Mới hơn Cũ hơn