0
0
Lập trình
Admin Team
Admin Teamtechmely

Xây Dựng Hệ Thống Quản Lý Người Dùng Toàn Diện Với Prisma Và Vue 3

Đăng vào 3 tuần trước

• 10 phút đọc

Xây Dựng Hệ Thống Quản Lý Người Dùng Toàn Diện Với Prisma Và Vue 3 (Nuxt 3)

Trong bài viết này, chúng ta sẽ khám phá cách xây dựng một hệ thống quản lý người dùng hoàn chỉnh với các chức năng CRUD (Tạo, Đọc, Cập nhật, Xóa) sử dụng Prisma làm ORM cơ sở dữ liệu và Vue 3 với Nuxt 3 cho giao diện frontend. Ví dụ này minh họa các thực hành phát triển full-stack hiện đại với TypeScript.

Mục Lục

Cấu Trúc Dự Án

Dự án của chúng ta theo mô hình kiến trúc sạch sẽ:

  • Prisma cho lược đồ cơ sở dữ liệu và ORM
  • Nuxt 3 cho framework full-stack
  • Vue 3 với Composition API cho các thành phần phản ứng
  • SQLite làm cơ sở dữ liệu (dễ dàng chuyển đổi sang PostgreSQL/MySQL)

Lược Đồ Cơ Sở Dữ Liệu Với Prisma

Hãy bắt đầu với lược đồ Prisma của chúng ta (prisma/schema.prisma):

prisma Copy
datasource db {
  provider = "sqlite"
  url = "file:./dev.db"
}

generator client {
  provider = "prisma-client-js"
}

model User {
  id        Int   @id @default(autoincrement())
  email     String   @unique
  name      String
  created_at DateTime @default(now())
}

Lược đồ đơn giản này định nghĩa mô hình Người Dùng với:

  • ID tự tăng làm khóa chính
  • Ràng buộc email duy nhất
  • Trường tên bắt buộc
  • Dấu thời gian tự động cho việc tạo

Thiết Lập Prisma Client

Chúng ta tạo một file tiện ích cơ sở dữ liệu (server/utils/db.ts) để quản lý Prisma client:

typescript Copy
import { PrismaClient } from '@prisma/client'

const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }

export const prisma = globalForPrisma.prisma ?? new PrismaClient()

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma

Mô hình này đảm bảo rằng chúng ta không tạo ra nhiều instance Prisma trong quá trình phát triển (hot reloading) trong khi vẫn duy trì một client sạch sẽ trong môi trường sản xuất.

API Routes Với Nuxt 3

Tính năng định tuyến API dựa trên file của Nuxt 3 giúp việc tạo ra các endpoint RESTful trở nên dễ dàng. Dưới đây là các API routes hoàn chỉnh của chúng ta:

Lấy Tất Cả Người Dùng (server/api/users.get.ts)

typescript Copy
import { prisma } from '../utils/db'

export default defineEventHandler(async (event) => {
  const users = await prisma.user.findMany({
    orderBy: {
      id: 'desc'
    }
  })
  return users
})

Tạo Người Dùng Mới (server/api/users.post.ts)

typescript Copy
import { prisma } from '../utils/db'

export default defineEventHandler(async (event) => {
  const body = await readBody(event)
  if (!body?.email || !body?.name) {
    throw createError({ statusCode: 400, statusMessage: 'Email và tên là bắt buộc' })
  }
  return prisma.user.create({
    data: {
        name: body.name,
        email: body.email
    }
  })
})

Lấy Một Người Dùng (server/api/users/[id].get.ts)

typescript Copy
import { prisma } from '../../utils/db'

export default defineEventHandler(async (event) => {
  const id = Number(getRouterParam(event, 'id'))
  const user = await prisma.user.findUnique({
    where: { id }
  })
  if (!user) {
    throw createError({ statusCode: 404, statusMessage: 'Người dùng không tồn tại' })
  }
  return user
})

Cập Nhật Người Dùng (server/api/users/[id].put.ts)

typescript Copy
import { prisma } from '../../utils/db'

export default defineEventHandler(async (event) => {
  const id = Number(getRouterParam(event, 'id'))
  const b = await readBody(event)
  if (!b?.email || !b?.name) {
    throw createError({ statusCode: 400, statusMessage: 'Email và tên là bắt buộc' })
  }
  return prisma.user.update({
    where: { id },
    data: { name: b.name, email: b.email }
  })
})

Xóa Người Dùng (server/api/users/[id].delete.ts)

typescript Copy
import { prisma } from "~~/server/utils/db"

export default defineEventHandler(async (event) => {
  const id = Number(getRouterParam(event, 'id'))
  await prisma.user.delete({
    where: { id }
  })

  return {ok: true}
})

Các Thành Phần Frontend Vue 3

Bây giờ hãy xây dựng các thành phần Vue 3 của chúng ta với Composition API:

Trang Danh Sách Người Dùng (app/pages/users/index.vue)

vue Copy
<script setup lang="ts">
const { $notyf } = useNuxtApp()

const { data: users, refresh: refreshData, error, pending } = await useAsyncData('users', () => $fetch('/api/users'))

watch(error, (e) => {
    if (e) $notyf.error(e.message || 'Không thể tải người dùng')
}, { immediate: true })

async function remove(id: number) {
  if (!confirm('Bạn có chắc chắn muốn xóa người dùng này?')) return

  try {
    await $fetch(`/api/users/${id}`, { method: 'DELETE' })
    refreshData()
    $notyf.success('Người dùng đã được xóa thành công')
  } catch (e: any) {
    $notyf.error(e?.data?.message || e?.message || 'Không thể xóa người dùng')
  }
}
</script>

<template>
  <main class="wrap">
    <h1>Người Dùng</h1>
    <NuxtLink to="/users/new" class="btn">➕ Mới</NuxtLink>

    <p v-if="error">Lỗi: {{ error.message }}</p>
    <table v-else>
        <thead>
            <tr>
                <th>ID</th>
                <th>Tên</th>
                <th>Email</th>
            </tr>
        </thead>
        <tbody>
            <tr v-for="u in users" :key="u.id">
                <td>{{ u.id }}</td>
                <td>
                    <NuxtLink :to="`/users/${u.id}`">{{ u.name }}</NuxtLink>
                </td>
                <td>{{ u.email }}</td>
                <td>
                    <button @click="remove(u.id)">
                        {{ pending ? 'Đang xóa...' : 'Xóa' }}
                    </button>
                </td>
            </tr>
        </tbody>
    </table>
  </main>
</template>

Trang Tạo Người Dùng Mới (app/pages/users/new.vue)

vue Copy
<script setup lang="ts">
const form = reactive({
  name: '',
  email: ''
})
const err = ref<string | null>(null)
const router = useRouter()

async function submit() {
    err.value = null
    try {
        await $fetch('/api/users', { method: 'POST', body: form })
        router.push('/users')
    } catch (e: any) {
        err.value = e?.data?.message || e?.message || 'Không thể tạo người dùng'
    }
}
</script>

<template>
  <main class="wrap">
    <h1>Người Dùng Mới</h1>
    <form @submit.prevent="submit">
        <input v-model="form.name" placeholder="Tên" required />
        <input v-model="form.email" placeholder="Email" required />
        <button>Tạo</button>
        <p v-if="err" class="err">{{ err }}</p>
    </form>
  </main>
</template>

Trang Chỉnh Sửa Người Dùng (app/pages/users/[id].vue)

vue Copy
<script setup lang="ts">
const route = useRoute()
const {data: user, error, refresh} = await useAsyncData(`user-${route.params.id}`, () => $fetch(`/api/users/${route.params.id}`))
const form = reactive({name: user.value?.name ?? '', email: user.value?.email ?? ''})
const err = ref<string | null>(null)

async function save()
{
    err.value = null
    try {
        await $fetch(`/api/users/${route.params.id}`, { method: 'PUT', body: form })
        await refresh()
    } catch (e: any) {
        err.value = e?.data?.message || e?.message || 'Không thể cập nhật người dùng'
    }
}
</script>

<template>
  <main class="wrap">
    <h1>Người Dùng #{{ route.params.id }}</h1>
    <p v-if="error">Lỗi: {{ error.message }}</p>
    <form v-else @submit.prevent="save">
        <input v-model="form.name" required />
        <input v-model="form.email" required />
        <button>Lưu</button>
        <p v-if="err" class="err">{{ err }}</p>
    </form>
  </main>
</template>

Tính Năng Chính & Lợi Ích

An Toàn Kiểu

  • Prisma tự động tạo các kiểu TypeScript từ lược đồ của bạn
  • Vue 3 + TypeScript cung cấp kiểm tra kiểu tại thời điểm biên dịch
  • Đảm bảo an toàn kiểu từ cơ sở dữ liệu đến UI

Mô Hình Hiện Đại

  • Composition API cho việc tổ chức mã tốt hơn và tái sử dụng
  • Định tuyến dựa trên file với khả năng tự động sinh route của Nuxt 3
  • Server-side rendering cho SEO và hiệu suất tốt hơn

Trải Nghiệm Phát Triển

  • Hot reloading trong quá trình phát triển
  • Tự động sinh API route dựa trên cấu trúc file
  • Xử lý lỗi tích hợp với mã trạng thái HTTP hợp lệ

Quản Lý Dữ Liệu

  • Dữ liệu phản ứng với hệ thống phản ứng của Vue
  • Cập nhật lạc quan và phản hồi UI theo thời gian thực
  • Xử lý lỗi với thông báo thân thiện với người dùng

Hệ Thống Thông Báo Với Notyf

Để cải thiện trải nghiệm người dùng, chúng ta tích hợp Notyf cho các thông báo tinh tế. Đầu tiên, hãy tạo một plugin Nuxt (app/plugins/notyf.client.ts):

typescript Copy
import { Notyf } from 'notyf'
import 'notyf/notyf.min.css'

export default defineNuxtPlugin(() => {
    const notyf = new Notyf({
        duration: 3000,
        position: {
            x: 'right',
            y: 'top'
        },
        types: [
            {type: 'success', background: '#000', icon: false},
            {type: 'error', background: '#000', icon: false},
            {type: 'info', background: '#000', icon: false},
        ]
    })

    return {provide: {notyf}}
})

Plugin này:

  • Tự động chèn CSS Notyf trên toàn cầu
  • Tùy chỉnh giao diện thông báo với nền đen
  • Đặt vị trí thông báo ở góc trên bên phải
  • Có sẵn trên tất cả các thành phần thông qua useNuxtApp()

Các thông báo được sử dụng xuyên suốt các thành phần Vue của chúng ta để phản hồi cho người dùng về các thao tác CRUD, như đã thấy trong trang danh sách người dùng nơi chúng ta hiển thị thông báo thành công/lỗi cho các thao tác xóa.

Kết Luận

Ví dụ này minh họa cách phát triển web hiện đại có thể vừa mạnh mẽ vừa thân thiện với lập trình viên. Sự kết hợp của:

  • Prisma cho các thao tác cơ sở dữ liệu an toàn kiểu
  • Vue 3 với Composition API cho các UI phản ứng
  • Nuxt 3 cho phát triển full-stack

Tạo ra một nền tảng vững chắc để xây dựng các ứng dụng web có thể mở rộng. Toàn bộ hệ thống CRUD được thực hiện với ít mã khởi tạo trong khi vẫn duy trì sự an toàn kiểu và trải nghiệm phát triển xuất sắc. Hệ thống định tuyến dựa trên file, tự động sinh API, và quản lý dữ liệu phản ứng làm cho bộ công nghệ này đặc biệt phù hợp cho việc prototyping nhanh và các ứng dụng sản xuất.

Các Thực Hành Tốt Nhất

  • Thực hiện kiểm tra kiểu với TypeScript để giảm thiểu lỗi.
  • Sử dụng các thông báo rõ ràng để thông báo cho người dùng về trạng thái của các thao tác CRUD.

Những Cạm Bẫy Thường Gặp

  • Đảm bảo rằng tất cả các trường bắt buộc được điền trước khi gửi yêu cầu.
  • Kiểm tra lỗi khi gọi API để xử lý các tình huống không mong muốn.

Mẹo Tối Ưu Hiệu Suất

  • Sử dụng caching cho các dữ liệu không thay đổi thường xuyên để cải thiện thời gian tải.
  • Tối ưu hóa các truy vấn Prisma để giảm thiểu thời gian phản hồi.

Phần Hỏi & Đáp

H: Có thể thay thế SQLite bằng cơ sở dữ liệu nào khác không?
Đ: Có, bạn có thể dễ dàng chuyển đổi sang PostgreSQL hoặc MySQL.

H: Làm thế nào để triển khai ứng dụng này lên môi trường thực tế?
Đ: Bạn có thể sử dụng dịch vụ như Vercel hoặc Heroku để triển khai ứng dụng Nuxt của bạn.

Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào