Ở Day 1, mình đã làm quen với Next.js 16, App Router, cấu trúc thư mục và cách tạo các trang giao diện. Tuy nhiên, một ứng dụng thực tế không thể chỉ hiển thị nội dung viết sẵn trong code. Nó thường cần lấy dữ liệu từ backend, xử lý dữ liệu và hiển thị cho người dùng.
Trong bài này, mình sẽ xây dựng một ví dụ nhỏ gồm hai phần:
- Frontend: Next.js 16 sử dụng App Router và TypeScript.
- Backend: ASP.NET Core Web API cung cấp dữ liệu bài viết.
Mục tiêu là hiểu được toàn bộ luồng dữ liệu từ lúc Next.js gửi request, ASP.NET Core xử lý request, trả về JSON cho đến khi dữ liệu xuất hiện trên giao diện.
1. Mục tiêu học tập
Sau khi hoàn thành bài viết, mình muốn có thể:
- Hiểu request, response, HTTP method và status code.
- Phân biệt routing của Next.js với routing của ASP.NET Core API.
- Phân biệt route parameter và query parameter.
- Tạo API GET để lấy danh sách và chi tiết bài viết.
- Hiểu cách ASP.NET Core trả về JSON.
- Sử dụng fetch() và async/await trong Next.js Server Component.
- Sử dụng TypeScript để mô tả kiểu dữ liệu nhận từ API.
- Kiểm tra lỗi và debug khi frontend không lấy được dữ liệu.
2. Tổng quan về HTTP và REST API
2.1. Request và Response
Khi trình duyệt hoặc một ứng dụng gửi HTTP request đến server, server sẽ xử lý request rồi trả về HTTP response.
Một request thường có các thành phần:
- Method: thao tác muốn thực hiện, ví dụ GET hoặc POST.
- URL: địa chỉ của tài nguyên cần truy cập.
- Headers: thông tin bổ sung của request.
- Body: dữ liệu gửi kèm, thường dùng khi tạo hoặc cập nhật dữ liệu.
Một response thường có:
- Status code: kết quả xử lý request.
- Headers: metadata của response.
- Body: dữ liệu trả về, ví dụ JSON.
2.2. Các HTTP method thường gặp
| Method | Mục đích | Ví dụ |
|---|---|---|
| GET | Lấy dữ liệu | GET /api/posts |
| POST | Tạo tài nguyên mới | POST /api/posts |
| PUT | Thay thế toàn bộ tài nguyên | PUT /api/posts/1 |
| PATCH | Cập nhật một phần tài nguyên | PATCH /api/posts/1 |
| DELETE | Xóa tài nguyên | DELETE /api/posts/1 |
Trong Day 2, mình sẽ tập trung vào GET. POST sẽ được giới thiệu ở phần mở rộng để hiểu cách gửi dữ liệu lên backend.
2.3. Một số status code quan trọng
| Status code | Ý nghĩa |
|---|---|
| 200 OK | Request được xử lý thành công. |
| 201 Created | Tài nguyên mới được tạo thành công. |
| 400 Bad Request | Request không hợp lệ. |
| 404 Not Found | Không tìm thấy tài nguyên. |
| 500 Internal Server Error | Server gặp lỗi khi xử lý request. |
3. Phân biệt routing của Next.js và ASP.NET Core
Đây là hai khái niệm dễ nhầm lẫn khi bắt đầu làm ứng dụng full-stack. Cả hai đều sử dụng URL, nhưng đảm nhiệm những nhiệm vụ khác nhau.
3.1. Next.js routing
Next.js App Router sử dụng cấu trúc thư mục trong thư mục app để xác định các route của giao diện.
src/
app/
page.tsx
posts/
page.tsx
[id]/
page.tsx
Cấu trúc trên tạo ra các trang:
/: trang chủ./posts: danh sách bài viết./posts/1: trang chi tiết bài viết có ID bằng 1.
Nếu dự án không sử dụng thư mục src, các thư mục trên sẽ nằm trực tiếp trong app/.
3.2. ASP.NET Core API routing
ASP.NET Core sử dụng attribute routing để xác định endpoint của API. Ví dụ:
[ApiController]
[Route("api/[controller]")]
public class PostsController : ControllerBase
{
[HttpGet]
public IActionResult GetPosts()
{
return Ok();
}
}
Với controller tên PostsController, route cơ sở là
/api/posts. Attribute [HttpGet] xác định
endpoint xử lý HTTP GET.
Vì vậy, /posts và /api/posts không phải
cùng một route:
/postslà trang giao diện Next.js./api/postslà endpoint lấy dữ liệu từ ASP.NET Core.
4. Xây dựng ASP.NET Core Web API
Để tập trung vào routing và data fetching, mình sẽ sử dụng dữ liệu giả lập trong bộ nhớ. Chưa cần database hoặc Entity Framework Core.
4.1. Tạo project
Mở terminal và tạo project Web API:
dotnet new webapi -n BlogApi
cd BlogApi
Cài đặt Swagger
dotnet add package Swashbuckle.AspNetCore
Cấu hình Swagger
var builder = WebApplication.CreateBuilder(args);
// OpenAPI
builder.Services.AddOpenApi();
// Register Controller-based APIs
builder.Services.AddControllers();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint(
"/openapi/v1.json",
"OpenAPI V1");
});
}
app.UseHttpsRedirection();
// Existing Minimal API
var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild",
"Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
app.MapGet("/weatherforecast", () =>
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
})
.WithName("GetWeatherForecast");
// Map Controller routes
app.MapControllers();
app.Run();
record WeatherForecast(
DateOnly Date,
int TemperatureC,
string? Summary)
{
public int TemperatureF =>
32 + (int)(TemperatureC / 0.5556);
}
Mở project bằng IDE, tạo thư mục Controllers và thêm file
PostsController.cs.
4.2. Tạo model Post
Tạo file Models/Post.cs:
namespace BlogApi.Models;
public record Post(
int Id,
string Title,
string Description
);
Record biểu diễn dữ liệu của một bài viết. Mỗi bài viết có ID, tiêu đề và mô tả.
4.3. Tạo PostsController
Tạo file Controllers/PostsController.cs:
using BlogApi.Models;
using Microsoft.AspNetCore.Mvc;
namespace BlogApi.Controllers;
[ApiController]
[Route("api/[controller]")]
public class PostsController : ControllerBase
{
private static readonly List<Post> Posts =
[
new(1, "Learn Next.js", "Introduction to Next.js"),
new(2, "Learn ASP.NET Core", "Build a REST API"),
new(3, "Learn Umbraco", "Introduction to Headless CMS")
];
// GET /api/posts
// GET /api/posts?search=Next
[HttpGet]
public IActionResult GetPosts(
[FromQuery] string? search)
{
IEnumerable<Post> result = Posts;
if (!string.IsNullOrWhiteSpace(search))
{
result = result.Where(post =>
post.Title.Contains(
search,
StringComparison.OrdinalIgnoreCase));
}
return Ok(result);
}
// GET /api/posts/1
[HttpGet("{id:int}")]
public IActionResult GetPost(int id)
{
var post = Posts.FirstOrDefault(
item => item.Id == id);
if (post is null)
{
return NotFound();
}
return Ok(post);
}
}
Controller này có hai endpoint GET. Endpoint đầu tiên lấy danh sách bài viết và có thể lọc theo tiêu đề. Endpoint thứ hai lấy một bài viết theo ID.
Dữ liệu hiện được lưu trong một danh sách tĩnh để học tập. Đây không phải cách lưu dữ liệu phù hợp cho ứng dụng production: dữ liệu có thể bị mất khi ứng dụng khởi động lại và việc thay đổi dữ liệu đồng thời cần được xử lý cẩn thận.
4.4. Hiểu cách routing hoạt động
Hãy phân tích các attribute trong controller:
[ApiController]: cung cấp các hành vi dành cho API controller.[Route("api/[controller]")]: định nghĩa route cơ sở. Token[controller]được thay bằng tên controller sau khi bỏ hậu tố Controller.[HttpGet]: khai báo endpoint xử lý GET tại route cơ sở.[HttpGet("{id:int}")]: khai báo endpoint GET có route parameter tên id và ràng buộc kiểu số nguyên.[FromQuery]: lấy giá trị từ query string.Ok(...): trả về HTTP 200 cùng dữ liệu.NotFound(): trả về HTTP 404.
5. Route parameter và query parameter
5.1. Route parameter
Route parameter là một phần của đường dẫn URL, thường dùng để xác định một tài nguyên cụ thể.
Ví dụ:
GET /api/posts/1
Trong controller, đoạn code sau lấy ID từ URL:
[HttpGet("{id:int}")]
public IActionResult GetPost(int id)
{
var post = Posts.FirstOrDefault(
item => item.Id == id);
if (post is null)
{
return NotFound();
}
return Ok(post);
}
Khi gọi /api/posts/1, ASP.NET Core bind giá trị 1 vào
tham số id.
5.2. Query parameter
Query parameter nằm sau dấu ? trong URL. Nhiều tham số
được phân tách bằng dấu &.
GET /api/posts?search=Next
Ở đây, search là query parameter. ASP.NET Core lấy giá
trị này thông qua [FromQuery].
[HttpGet]
public IActionResult GetPosts(
[FromQuery] string? search)
{
// Filter posts using search.
// Return the result.
}
Trong ứng dụng thực tế, query parameter thường dùng cho tìm kiếm, lọc, phân trang hoặc sắp xếp dữ liệu.
Ví dụ:
GET /api/posts?page=1&pageSize=10
GET /api/posts?search=Next
GET /api/posts?search=Next&page=1
Lưu ý: ví dụ controller ở trên mới xử lý search. Các tham số page và pageSize chỉ là ví dụ về URL, chưa được triển khai phân trang.
6. Hiểu JSON response
Khi API xử lý thành công và gọi return Ok(result),
ASP.NET Core sẽ serialize dữ liệu thành JSON và trả về client.
Ví dụ response của GET /api/posts:
[
{
"id": 1,
"title": "Learn Next.js",
"description": "Introduction to Next.js"
},
{
"id": 2,
"title": "Learn ASP.NET Core",
"description": "Build a REST API"
},
{
"id": 3,
"title": "Learn Umbraco",
"description": "Introduction to Headless CMS"
}
]
Response của GET /api/posts/1 có dạng một object:
{
"id": 1,
"title": "Learn Next.js",
"description": "Introduction to Next.js"
}
Nếu gọi GET /api/posts/999, API trả về HTTP 404 khi
không tìm thấy bài viết.
JSON là định dạng dữ liệu, không phải giao diện. Backend trả về dữ liệu; frontend quyết định cách hiển thị dữ liệu đó.
7. Chạy và kiểm tra API
Khởi động project ASP.NET Core:
dotnet run
Hãy xem địa chỉ HTTP và HTTPS mà ứng dụng in ra trong terminal, hoặc
xem file Properties/launchSettings.json. Địa chỉ có thể
khác nhau giữa các máy.
Nếu project có cấu hình Swagger, hãy mở trang Swagger theo URL được hiển thị hoặc cấu hình trong project. Bạn cũng có thể gọi các endpoint trực tiếp bằng trình duyệt hoặc công cụ gửi HTTP request.
Thử lần lượt:
GET /api/postsGET /api/posts?search=NextGET /api/posts/1GET /api/posts/999
Nếu kết quả trả về đúng, backend đã sẵn sàng để Next.js lấy dữ liệu. Hãy ghi lại base URL thực tế của API để sử dụng ở phần tiếp theo.
8. Kết nối Next.js 16 với ASP.NET Core API
8.1. Khai báo kiểu dữ liệu TypeScript
Trong Next.js, tạo hoặc mở file app/posts/page.tsx.
Nếu project dùng thư mục src, đường dẫn sẽ là
src/app/posts/page.tsx.
Trước tiên, khai báo kiểu dữ liệu mà API trả về:
type Post = {
id: number;
title: string;
description: string;
};
Kiểu Post giúp TypeScript kiểm tra cách sử dụng dữ liệu trong code. Tuy nhiên, khai báo type không tự xác thực JSON ở runtime. Nếu API trả về dữ liệu không đúng cấu trúc, ứng dụng vẫn cần có cơ chế kiểm tra dữ liệu khi yêu cầu đó quan trọng.
8.2. Viết hàm gọi API
async function getPosts(): Promise<Post[]> {
const response = await fetch(
"https://localhost:7001/api/posts"
);
if (!response.ok) {
throw new Error("Failed to fetch posts");
}
return response.json();
}
Trong ví dụ này, https://localhost:7001 chỉ là địa chỉ
minh họa. Hãy thay bằng base URL thực tế của ASP.NET Core API.
Phân tích từng phần:
async: cho phép sử dụng await bên trong hàm.fetch(): gửi HTTP request đến API.await: chờ Promise hoàn thành.response.ok: kiểm tra status code có nằm trong nhóm 200–299 hay không.response.json(): đọc response body và parse JSON.Promise<Post[]>: mô tả kiểu dữ liệu mà hàm dự kiến trả về.
Tại sao phải kiểm tra response.ok? Vì fetch không tự
throw error chỉ vì server trả về HTTP 404 hoặc 500. Nếu không kiểm
tra status, code có thể tiếp tục đọc response như thể request thành
công.
8.3. Hiển thị danh sách bài viết
Tiếp theo, sử dụng hàm getPosts trong Server Component:
type Post = {
id: number;
title: string;
description: string;
};
async function getPosts(): Promise<Post[]> {
const response = await fetch(
"https://localhost:7001/api/posts"
);
if (!response.ok) {
throw new Error("Failed to fetch posts");
}
return response.json();
}
export default async function PostsPage() {
const posts = await getPosts();
return (
<main>
<h1>Danh sách bài viết</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>
<h2>{post.title}</h2>
<p>{post.description}</p>
</li>
))}
</ul>
</main>
);
}
Trong đoạn code trên, PostsPage là Server Component.
Hàm getPosts chạy ở phía server của Next.js, lấy dữ liệu từ ASP.NET
Core và dùng dữ liệu đó để tạo giao diện.
Không cần thêm "use client" chỉ để gọi fetch trong
Server Component. Bạn chỉ cần Client Component khi có nhu cầu tương
tác phía trình duyệt, chẳng hạn sử dụng useState hoặc xử lý sự kiện
onClick.
9. Hiểu luồng dữ liệu giữa hai ứng dụng
Sơ đồ dưới đây tóm tắt quá trình khi người dùng truy cập trang
/posts.
flowchart TD
A["Browser: truy cập /posts"] --> B["Next.js Server Component"]
B --> C["fetch GET /api/posts"]
C --> D["ASP.NET Core PostsController"]
D --> E["Xử lý dữ liệu và serialize JSON"]
E --> F["HTTP response: 200 OK"]
F --> G["Next.js parse JSON"]
G --> H["Render danh sách bài viết"]
H --> I["Browser hiển thị HTML"]
Điểm cần nhớ: trong ví dụ này, Next.js Server Component gọi API từ server. Vì vậy, request fetch không nhất thiết xuất hiện trong tab Network của trình duyệt như một request trực tiếp từ JavaScript phía client. Hãy kiểm tra terminal của Next.js và log phía ASP.NET Core khi debug luồng server-to-server.
10. Xử lý lỗi khi fetch dữ liệu
Đoạn code hiện tại throw Error khi response không thành công. Đây là cách tối thiểu để không tiếp tục xử lý một response lỗi như dữ liệu thành công.
Trong Next.js App Router, bạn có thể thêm file
app/posts/error.tsx để cung cấp giao diện lỗi cho route
này. File error boundary phải là Client Component.
"use client";
export default function PostsError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<main>
<h1>Không thể tải danh sách bài viết</h1>
<p>Đã xảy ra lỗi khi tải dữ liệu.</p>
<button onClick={() => reset()}>
Thử lại
</button>
</main>
);
}
Không nên hiển thị thông tin lỗi nội bộ nhạy cảm trực tiếp cho người dùng. Trong ví dụ trên, giao diện chỉ đưa ra thông báo chung.
Bạn cũng có thể thêm file app/posts/loading.tsx để
Next.js hiển thị giao diện chờ trong lúc route đang tải.
Lưu ý: error.tsx và loading.tsx có phạm vi hoạt động theo route segment. Chúng không thay thế việc xử lý đầy đủ mọi loại lỗi nghiệp vụ hoặc mọi trường hợp API trả về dữ liệu không hợp lệ.
11. Những vấn đề thường gặp khi kết nối API
11.1. Sai URL hoặc port
Nếu Next.js gọi nhầm port, request có thể thất bại vì không kết nối được đến backend. Hãy xác nhận URL thực tế từ terminal hoặc launchSettings.json.
11.2. Lỗi HTTPS certificate khi chạy local
Nếu ASP.NET Core sử dụng HTTPS với chứng chỉ phát triển chưa được tin cậy, Node.js hoặc Next.js có thể từ chối kết nối. Hãy cấu hình và tin cậy development certificate đúng cách. Không nên tắt kiểm tra chứng chỉ TLS để làm cách khắc phục lâu dài.
11.3. Lỗi CORS
CORS chủ yếu ảnh hưởng đến request được thực hiện trực tiếp từ trình duyệt. Trong ví dụ Server Component gọi API từ Next.js server, CORS của trình duyệt không phải nguyên nhân thông thường gây lỗi cho request đó.
Nếu chuyển việc gọi API sang Client Component, bạn cần xem xét chính sách CORS của ASP.NET Core và cho phép origin frontend phù hợp.
11.4. API trả về lỗi nhưng fetch không tự throw
Hãy kiểm tra response.ok trước khi đọc JSON. Fetch có
thể hoàn thành Promise bình thường ngay cả khi status là 404 hoặc
500.
11.5. Không thấy request trong Browser Network
Nếu request chạy trong Server Component, hãy xem terminal của Next.js. Browser Network chủ yếu phản ánh request mà trình duyệt thực hiện. Đây là khác biệt quan trọng giữa server-side fetching và client-side fetching.
12. Debug ứng dụng
Khi dữ liệu không hiển thị, nên kiểm tra theo thứ tự:
- Gọi API trực tiếp bằng Swagger hoặc công cụ HTTP để xác nhận backend hoạt động.
- Kiểm tra URL và port trong hàm fetch của Next.js.
- Đặt breakpoint hoặc log trong PostsController để xác nhận API có nhận request.
- Kiểm tra terminal của Next.js để tìm lỗi kết nối, HTTPS hoặc lỗi render.
- Kiểm tra response status và JSON structure.
- Kiểm tra TypeScript type và cách render dữ liệu trong JSX.
Có thể dùng console.log() trong Server Component để
kiểm tra dữ liệu. Log này xuất hiện trong terminal của Next.js,
không phải Browser Console.
13. POST — gửi dữ liệu lên backend (giới thiệu)
GET dùng để lấy dữ liệu. Khi muốn tạo một bài viết mới, client thường gửi POST request với JSON trong request body.
Ví dụ HTTP request:
POST /api/posts
Content-Type: application/json
{
"title": "Learn Next.js",
"description": "My first post"
}
Một endpoint POST thường thực hiện các bước:
- Nhận dữ liệu từ request body.
- Validate dữ liệu đầu vào.
- Tạo tài nguyên mới.
- Trả về HTTP 201 Created nếu tạo thành công.
Trong bài này, POST chỉ là phần giới thiệu. Để hoàn thiện chức năng tạo bài viết, cần bổ sung request model, validation, endpoint POST và form gửi dữ liệu từ frontend. Mình sẽ dành bài thực hành riêng cho phần đó.
14. Bài tập thực hành
Bài 1 — Lọc bài viết
- Gọi
/api/posts?search=Next. - Thử các từ khóa khác nhau.
- Kiểm tra kết quả khi không có bài viết phù hợp.
Bài 2 — Tạo trang chi tiết
- Tạo route Next.js
/posts/[id]. - Đọc ID từ URL.
- Gọi API
/api/posts/{id}. - Hiển thị tiêu đề và mô tả của bài viết.
- Xử lý trường hợp API trả về 404.
Bài 3 — Tìm hiểu POST
- Tạo request model để nhận tiêu đề và mô tả.
- Viết endpoint POST trong ASP.NET Core.
- Thử gửi JSON bằng Swagger hoặc công cụ HTTP.
- Kiểm tra status code và response khi tạo thành công.
15. Câu hỏi tự kiểm tra
- Khác biệt giữa
/postsvà/api/postslà gì? - Route parameter khác query parameter như thế nào?
- Tại sao phải kiểm tra
response.oksau khi gọi fetch? - Vai trò của
response.json()là gì? - Tại sao TypeScript type không tự xác thực dữ liệu JSON lúc runtime?
- Vì sao fetch trong Server Component thường không xuất hiện như request trực tiếp của ứng dụng trong Browser Network?
- Khi nào cần dùng Client Component thay vì Server Component?
- HTTP 200, 201 và 404 có ý nghĩa gì?
16. Tổng kết
Qua Day 2, mình đã kết nối được Next.js 16 với ASP.NET Core Web API và hiểu các bước chính của luồng dữ liệu:
- ASP.NET Core định nghĩa endpoint bằng routing và HTTP attributes.
- Route parameter xác định tài nguyên; query parameter hỗ trợ tìm kiếm và lọc.
- API trả về status code và JSON response.
- Next.js sử dụng fetch() để lấy dữ liệu từ backend.
- TypeScript mô tả cấu trúc dữ liệu và JSX hiển thị dữ liệu lên giao diện.
- Server Component cho phép fetch dữ liệu từ phía server mà không cần chuyển trang thành Client Component.
Trong ứng dụng thực tế, backend có thể sử dụng database thay vì danh sách dữ liệu giả lập. Phần fetch cũng cần được thiết kế cùng với chiến lược cache, revalidation, bảo mật và xử lý lỗi phù hợp.
Bước tiếp theo là xây dựng trang chi tiết bằng dynamic route
/posts/[id], sau đó tìm hiểu cách lấy nội dung từ một
Headless CMS như Umbraco.
Nhận xét