Loading header...

Hướng dẫn sử dụng API

Giới thiệu

Dự án sử dụng hệ thống API RESTful với các tiện ích mạnh mẽ cho việc lấy dữ liệu. Hệ thống được thiết kế để dễ dàng sử dụng, linh hoạt và hiệu quả.

Cấu trúc API

API Client

  • api-client.ts: Cấu hình axios với interceptors, xử lý URL và chuyển đổi dữ liệu
  • api-service.ts: Các hàm gọi API cơ bản (fetchItems, fetchItem, fetchItemById)
  • hooks/use-api.ts: React hooks sử dụng React Query để quản lý trạng thái và caching

Sử dụng API trong Client Components

Sử dụng useApi Hook

1import { useApi } from "@/hooks/use-api"; 2import type { Post } from "@/types/post"; 3 4function BlogList() { 5 const { data, isLoading, error } = useApi<Post>("posts", { 6 fields: ["title", "slug->vi", "created_at", "cover"], 7 filter: { 8 status: { _eq: "published" }, 9 }, 10 sort: [ 11 { column: "position", order: "desc" }, 12 { column: "created_at", order: "desc" }, 13 ], 14 limit: 10, 15 page: 1, 16 }); 17 18 if (isLoading) return <div>Đang tải...</div>; 19 if (error) return <div>Đã xảy ra lỗi</div>; 20 21 return ( 22 <div> 23 {data?.data.map((post) => ( 24 <div key={post.id}>{post.title}</div> 25 ))} 26 </div> 27 ); 28}

Lấy chi tiết một mục theo slug

1import { useApi } from "@/hooks/use-api"; 2import type { Post } from "@/types/post"; 3 4function PostDetail({ slug }: { slug: string }) { 5 const { data, isLoading } = useApi<Post>("posts", { 6 fields: ["title", "slug->toRaw", "content", "post_categories"], 7 filter: { "slug->vi": { _eq: slug } }, 8 limit: 1, 9 paginate: false, 10 }); 11 12 const post = data?.data[0]; 13 14 if (isLoading) return <div>Đang tải...</div>; 15 if (!post) return <div>Không tìm thấy bài viết</div>; 16 17 return ( 18 <div> 19 <h1>{post.title}</h1> 20 <div dangerouslySetInnerHTML={{ __html: post.content || "" }} /> 21 </div> 22 ); 23}

Sử dụng API trong Server Components

1import apiService from "@/services/api-service"; 2import { Post } from "@/types/post"; 3 4export default async function BlogPage() { 5 // Lấy dữ liệu blog trong server component 6 const blogData = await apiService.fetchItems<Post>("posts", { 7 fields: ["title", "slug", "created_at"], 8 filter: { status: { _eq: "published" } }, 9 limit: 10, 10 }); 11 12 return ( 13 <div> 14 {blogData.data.map((post) => ( 15 <div key={post.id}>{post.title}</div> 16 ))} 17 </div> 18 ); 19}

Tham số API

Fields

Chỉ định các trường cần lấy:

1fields: ["title", "slug", "created_at"]

Trường đa ngôn ngữ:

  • Lấy giá trị ngôn ngữ mặc định: fields: ["slug"]
  • Lấy giá trị ngôn ngữ cụ thể: fields: ["slug->vi"]
  • Lấy tất cả giá trị ngôn ngữ: fields: ["slug->toRaw"]

Lấy dữ liệu của quan hệ:

1fields: ["post_categories.name", "author.name"]

Filter

Lọc dữ liệu với nhiều toán tử:

1filter: { 2 // Bằng 3 status: { _eq: "published" }, 4 5 // Khác 6 status: { _neq: "draft" }, 7 8 // Chứa chuỗi (like) 9 title: { _like: "Nextjs" }, 10 11 // Lớn hơn, nhỏ hơn 12 created_at: { _gt: "2023-01-01" }, 13 14 // Trong danh sách 15 id: { _in: [1, 2, 3] }, 16 17 // Lọc theo trường đa ngôn ngữ 18 "slug->vi": { _eq: "bai-viet" }, 19 20 // Toán tử logic OR 21 _or: [ 22 { title: { _like: "react" } }, 23 { content: { _like: "react" } } 24 ] 25}

Sắp xếp

1sort: [ 2 { column: "position", order: "desc" }, 3 { column: "created_at", order: "desc" } 4]

Phân trang

1// Với phân trang 2limit: 10, 3page: 1, 4 5// Không phân trang 6paginate: false, 7limit: 1,

Xử lý ngôn ngữ

Thiết lập ngôn ngữ

1import { setApiLocale } from "@/services/api-client"; 2 3// Thiết lập ngôn ngữ cho API 4setApiLocale("en");

Lấy dữ liệu theo ngôn ngữ

1// Lấy bài viết theo slug tiếng Việt 2const post = await apiService.fetchItem("posts", slug, "vi", [ 3 "title", "content", "post_categories" 4]);

Phát triển offline

Dự án hỗ trợ phát triển offline thông qua dữ liệu mẫu:

1import { mockPosts } from "@/mockdata/blog-data"; 2 3// Sử dụng dữ liệu mẫu 4const posts = mockPosts.data;

Các lưu ý quan trọng

  1. Hiệu suất: Chỉ định fields cần thiết để giảm kích thước phản hồi
  2. Caching: React Query tự động caching các yêu cầu API
  3. Xử lý lỗi: Luôn xử lý trường hợp isLoading và error
  4. Type-safety: Sử dụng TypeScript generic cho kiểu dữ liệu phản hồi
  5. Server/Client: Sử dụng apiService cho Server Components và useApi cho Client Components

Ví dụ nâng cao

Tìm kiếm với thanh tìm kiếm

1import { useApi } from "@/hooks/use-api"; 2import { useState, useEffect } from "react"; 3import { useDebounce } from "@/hooks/use-debounce"; 4 5function SearchPosts() { 6 const [searchTerm, setSearchTerm] = useState(""); 7 const debouncedSearch = useDebounce(searchTerm, 500); 8 9 const { data, isLoading } = useApi( 10 "posts", 11 { 12 filter: { 13 _or: [ 14 { title: { _like: debouncedSearch } }, 15 { content: { _like: debouncedSearch } }, 16 ], 17 status: { _eq: "published" }, 18 }, 19 limit: 10, 20 }, 21 { 22 // Chỉ gọi API khi giá trị tìm kiếm không rỗng 23 enabled: debouncedSearch.length > 0, 24 } 25 ); 26 27 return ( 28 <div> 29 <input 30 type="text" 31 value={searchTerm} 32 onChange={(e) => setSearchTerm(e.target.value)} 33 placeholder="Tìm kiếm..." 34 /> 35 36 {isLoading ? ( 37 <div>Đang tìm kiếm...</div> 38 ) : ( 39 <div> 40 {data?.data.map((post) => ( 41 <div key={post.id}>{post.title}</div> 42 ))} 43 </div> 44 )} 45 </div> 46 ); 47}

Phân trang với react-query

1import { useApi } from "@/hooks/use-api"; 2import { useState } from "react"; 3 4function PaginatedList() { 5 const [page, setPage] = useState(1); 6 const pageSize = 10; 7 8 const { data, isLoading, isPreviousData } = useApi( 9 "posts", 10 { 11 filter: { status: { _eq: "published" } }, 12 sort: [{ column: "created_at", order: "desc" }], 13 limit: pageSize, 14 page, 15 }, 16 { 17 // Giữ dữ liệu trang trước trong khi tải dữ liệu trang mới 18 keepPreviousData: true, 19 } 20 ); 21 22 return ( 23 <div> 24 <div> 25 {data?.data.map((post) => ( 26 <div key={post.id}>{post.title}</div> 27 ))} 28 </div> 29 30 <div className="pagination"> 31 <button 32 onClick={() => setPage((old) => Math.max(old - 1, 1))} 33 disabled={page === 1} 34 > 35 Trước 36 </button> 37 38 <span> 39 Trang {page} / {data?.meta?.last_page || 1} 40 </span> 41 42 <button 43 onClick={() => setPage((old) => old + 1)} 44 disabled={ 45 isPreviousData || !data?.meta?.last_page || page >= data.meta.last_page 46 } 47 > 48 Sau 49 </button> 50 </div> 51 </div> 52 ); 53}

Đầu tư là chiến thuật. Cấu trúc là chiến lược.