- Giới thiệu tổng quan
- 1. Kiến trúc tổng quan
- 2. Khởi tạo dự án Strapi và bật plugin GraphQL
- 3. Tạo một API GraphQL tùy chỉnh với Apollo Server
- 4. Xác thực và phân quyền JWT
- 5. Tối ưu hiệu năng với DataLoader và Redis Cache
- 6. Triển khai môi trường Docker Compose
- 7. Kiểm thử và giám sát
- 8. Các lưu ý bảo mật và best practices
- Kết luận
Giới thiệu tổng quan
Trong môi trường phát triển hiện đại, API-first đang trở thành tiêu chuẩn cho các dự án đa nền tảng. GraphQL nổi bật với khả năng truy vấn linh hoạt, giảm thiểu over‑fetching và under‑fetching. Khi kết hợp Strapi v5 – một Headless CMS mã nguồn mở – với Apollo Server, chúng ta có thể xây dựng một lớp API GraphQL mạnh mẽ, dễ mở rộng và bảo mật. Bài viết này sẽ đi sâu vào kiến trúc, cách tùy chỉnh resolver, quản lý xác thực JWT, tối ưu hiệu năng và triển khai trên Docker.
![]()
1. Kiến trúc tổng quan
Strapi cung cấp sẵn plugin GraphQL, nhưng để khai thác tối đa khả năng tùy biến, chúng ta sẽ:
- Khởi tạo một dự án Strapi chuẩn.
- Cài đặt và cấu hình plugin
strapi-plugin-graphql. - Triển khai một Apollo Server độc lập, kết nối tới Strapi thông qua
httphoặcgraphql-request. - Áp dụng middleware xác thực JWT trước khi thực thi resolver.
- Cache kết quả truy vấn bằng Redis để giảm tải.
Kiến trúc này cho phép chúng ta tách rời phần business logic (Apollo) khỏi phần quản lý nội dung (Strapi), đồng thời dễ dàng mở rộng các micro‑service khác trong tương lai.
2. Khởi tạo dự án Strapi và bật plugin GraphQL
Bước 1: Tạo dự án Strapi
Sử dụng npx để tạo một dự án mới:
$ npx create-strapi-app my-cms --quickstart
Lệnh trên sẽ cài đặt Strapi, khởi động server tại http://localhost:1337 và tạo tài khoản admin.
Bước 2: Cài đặt plugin GraphQL
Trong thư mục dự án, chạy:
$ npm install @strapi/plugin-graphql
Sau khi cài đặt, bật plugin bằng cách chỉnh sửa config/plugins.js:
module.exports = {
// ...các plugin khác
graphql: {
enabled: true,
config: {
endpoint: "/graphql",
shadowCRUD: true,
playgroundAlways: true,
depthLimit: 7,
amountLimit: 100,
},
},
};
Khởi động lại Strapi để áp dụng cấu hình.
3. Tạo một API GraphQL tùy chỉnh với Apollo Server
3.1. Thiết lập dự án Apollo
Tạo một thư mục mới apollo-server và khởi tạo dự án Node.js:
$ mkdir apollo-server && cd apollo-server $ npm init -y $ npm install apollo-server graphql graphql-request jsonwebtoken redis ioredis
3.2. Định nghĩa schema và resolver
Giả sử Strapi có một collection type article với các trường title, content và author. Chúng ta sẽ viết một resolver để lấy danh sách bài viết kèm thông tin tác giả.
const { ApolloServer, gql } = require('apollo-server');
const { request, gql: gqlRequest } = require('graphql-request');
const jwt = require('jsonwebtoken');
const Redis = require('ioredis');
const redis = new Redis({ host: 'redis', port: 6379 });
// GraphQL endpoint của Strapi
const STRAPI_ENDPOINT = 'http://localhost:1337/graphql';
// Định nghĩa schema GraphQL cho Apollo
const typeDefs = gql`
type Article {
id: ID!
title: String!
content: String!
author: User!
}
type User {
id: ID!
username: String!
email: String!
}
type Query {
articles: [Article!]!
me: User
}
`;
// Resolver thực thi truy vấn tới Strapi
const resolvers = {
Query: {
articles: async (_, __, { token }) => {
// Kiểm tra cache Redis
const cacheKey = 'articles:all';
const cached = await redis.get(cacheKey);
if (cached) return JSON.parse(cached);
// Truy vấn GraphQL của Strapi
const query = gqlRequest`
query {
articles {
data {
id
attributes {
title
content
author {
data {
id
attributes {
username
email
}
}
}
}
}
}
}
`;
const response = await request(STRAPI_ENDPOINT, query, null, {
Authorization: `Bearer ${token}`,
});
// Chuyển đổi dữ liệu sang định dạng GraphQL của Apollo
const articles = response.articles.data.map(item => ({
id: item.id,
title: item.attributes.title,
content: item.attributes.content,
author: {
id: item.attributes.author.data.id,
username: item.attributes.author.data.attributes.username,
email: item.attributes.author.data.attributes.email,
},
}));
// Lưu vào cache trong 5 phút
await redis.setex(cacheKey, 300, JSON.stringify(articles));
return articles;
},
me: async (_, __, { token }) => {
if (!token) return null;
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// Giả sử Strapi có endpoint /users/me trả về thông tin người dùng
const query = gqlRequest`
query {
me {
id
username
email
}
}
`;
const response = await request(STRAPI_ENDPOINT, query, null, {
Authorization: `Bearer ${token}`,
});
return response.me;
},
},
};
// Context để truyền token từ header
const context = ({ req }) => {
const token = req.headers.authorization?.replace('Bearer ', '');
return { token };
};
const server = new ApolloServer({ typeDefs, resolvers, context });
server.listen({ port: 4000 }).then(({ url }) => {
console.log(`🚀 Apollo Server ready at ${url}`);
});
Trong đoạn code trên, chúng ta đã:
- Khởi tạo một
ApolloServerđộc lập. - Sử dụng
graphql-requestđể gọi API GraphQL của Strapi. - Áp dụng caching bằng Redis để giảm tải.
- Triển khai middleware
contextđể truyền JWT từ header.
4. Xác thực và phân quyền JWT
4.1. Cấu hình JWT trong Strapi
Strapi sử dụng JWT để bảo vệ các endpoint. Mở file config/plugins.js và thêm:
module.exports = {
// ...
usersPermissions: {
config: {
jwtSecret: process.env.JWT_SECRET || 'your-secret-key',
jwtExpiresIn: '7d',
},
},
};
Đảm bảo biến môi trường JWT_SECRET được đặt trong file .env của Strapi.
4.2. Tạo middleware xác thực trong Apollo
Chúng ta sẽ viết một middleware để kiểm tra token trước khi cho phép truy cập các resolver bảo mật.
// authMiddleware.js
const jwt = require('jsonwebtoken');
module.exports = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader) return res.status(401).json({ message: 'Missing Authorization header' });
const token = authHeader.replace('Bearer ', '');
try {
const payload = jwt.verify(token, process.env.JWT_SECRET);
req.user = payload; // gán thông tin người dùng vào request
next();
} catch (err) {
return res.status(401).json({ message: 'Invalid token' });
}
};
Sau đó, trong server.js (đoạn code Apollo ở trên), chúng ta có thể tích hợp middleware này bằng cách sử dụng express (Apollo Server hỗ trợ express middleware).
const express = require('express');
const { ApolloServer } = require('apollo-server-express');
const authMiddleware = require('./authMiddleware');
const app = express();
app.use(authMiddleware);
const server = new ApolloServer({ typeDefs, resolvers, context });
await server.start();
server.applyMiddleware({ app, path: '/graphql' });
app.listen({ port: 4000 }, () => {
console.log(`🚀 Server ready at http://localhost:4000${server.graphqlPath}`);
});
Với cách này, mọi request tới Apollo sẽ phải có token hợp lệ, đồng thời chúng ta có thể truy cập thông tin người dùng qua req.user trong resolver.
5. Tối ưu hiệu năng với DataLoader và Redis Cache
5.1. Giới thiệu DataLoader
DataLoader là một công cụ giúp batch và cache các truy vấn tới nguồn dữ liệu, giảm thiểu số lần gọi API nội bộ. Khi chúng ta cần lấy thông tin người dùng cho nhiều bài viết, DataLoader sẽ gom các yêu cầu lại thành một lần gọi.
const DataLoader = require('dataloader');
// Loader để lấy thông tin user dựa trên ID
const userLoader = new DataLoader(async ids => {
const query = gqlRequest`
query ($ids: [ID!]!) {
users(filters: { id: { $in: $ids } }) {
data {
id
attributes { username email }
}
}
}
`;
const variables = { ids };
const response = await request(STRAPI_ENDPOINT, query, variables, {
Authorization: `Bearer ${process.env.SYSTEM_TOKEN}`,
});
// Đảm bảo trả về mảng theo thứ tự ids
const userMap = {};
response.users.data.forEach(u => {
userMap[u.id] = {
id: u.id,
username: u.attributes.username,
email: u.attributes.email,
};
});
return ids.map(id => userMap[id] || null);
});
Trong resolver articles, thay vì gọi Strapi cho mỗi tác giả, chúng ta sử dụng userLoader.load(authorId) để batch.
5.2. Kết hợp Redis Cache cho DataLoader
DataLoader chỉ cache trong vòng đời của một request. Để cache lâu dài, chúng ta đồng bộ với Redis:
const cachedUserLoader = async id => {
const cacheKey = `user:${id}`;
const cached = await redis.get(cacheKey);
if (cached) return JSON.parse(cached);
const user = await userLoader.load(id);
await redis.setex(cacheKey, 3600, JSON.stringify(user)); // 1 giờ
return user;
};
Nhờ cách này, các truy vấn lặp lại trong các request tiếp theo sẽ được phục vụ ngay từ Redis, giảm độ trễ đáng kể.
6. Triển khai môi trường Docker Compose
6.1. Dockerfile cho Strapi
# Dockerfile (Strapi) FROM node:18-alpine WORKDIR /app COPY package*.json ./ RUN npm install --production COPY . . ENV NODE_ENV=production EXPOSE 1337 CMD ["npm", "run", "start"]
6.2. Dockerfile cho Apollo Server
# Dockerfile (Apollo) FROM node:18-alpine WORKDIR /app COPY package*.json ./ RUN npm install --production COPY . . EXPOSE 4000 CMD ["node", "server.js"]
6.3. docker‑compose.yml
version: "3.8"
services:
strapi:
build:
context: ./strapi
ports:
- "1337:1337"
environment:
- DATABASE_CLIENT=sqlite
- DATABASE_FILENAME=./data.db
- JWT_SECRET=${JWT_SECRET}
volumes:
- ./strapi:/app
depends_on:
- redis
apollo:
build:
context: ./apollo-server
ports:
- "4000:4000"
environment:
- STRAPI_ENDPOINT=http://strapi:1337/graphql
- JWT_SECRET=${JWT_SECRET}
depends_on:
- strapi
- redis
volumes:
- ./apollo-server:/app
redis:
image: redis:7-alpine
ports:
- "6379:6379"
command: ["redis-server", "--appendonly", "yes"]
Sau khi tạo các file trên, chạy docker-compose up -d --build để khởi động ba service: Strapi, Apollo và Redis. Các service sẽ giao tiếp qua mạng nội bộ Docker, giúp môi trường phát triển nhất quán.
7. Kiểm thử và giám sát
7.1. Kiểm thử tích hợp với Jest
Viết test cho resolver articles bằng jest và supertest:
const request = require('supertest');
const { createServer } = require('../server'); // hàm tạo Apollo server
describe('GraphQL Articles Query', () => {
let server;
beforeAll(async () => {
server = await createServer();
});
it('should return list of articles with author', async () => {
const query = `
query {
articles {
id
title
author { id username }
}
}
`;
const response = await request(server)
.post('/graphql')
.send({ query })
.set('Authorization', `Bearer ${process.env.SYSTEM_TOKEN}`);
expect(response.status).toBe(200);
expect(response.body.data.articles).toBeInstanceOf(Array);
expect(response.body.data.articles[0]).toHaveProperty('author');
});
});
7.2. Giám sát với Prometheus & Grafana
Thêm exporter cho Node.js (prom-client) vào Apollo Server:
const client = require('prom-client');
const collectDefaultMetrics = client.collectDefaultMetrics;
collectDefaultMetrics({ timeout: 5000 });
app.get('/metrics', async (req, res) => {
res.set('Content-Type', client.register.contentType);
res.end(await client.register.metrics());
});
Prometheus sẽ scrape endpoint /metrics, và Grafana hiển thị biểu đồ thời gian phản hồi, số lượng request, cache hit ratio, v.v.
8. Các lưu ý bảo mật và best practices
- Không để JWT secret trong mã nguồn. Dùng
.envvà công cụ quản lý secret (Vault, AWS Secrets Manager). - Giới hạn độ sâu truy vấn GraphQL. Cấu hình
depthLimittrong plugin Strapi để tránh query quá phức tạp. - Thực thi rate‑limiting. Sử dụng middleware
express-rate-limitcho Apollo để bảo vệ trước tấn công DoS. - Kiểm tra input. Dùng
Joihoặczodđể validate biến truyền vào GraphQL. - Áp dụng principle of least privilege. Tạo role trong Strapi chỉ cho phép đọc
articlevà viếtcommentnếu cần.
Kết luận
Việc kết hợp Strapi v5 với Apollo Server cho phép xây dựng một API GraphQL mạnh mẽ, linh hoạt và bảo mật. Nhờ các kỹ thuật như DataLoader, Redis cache, JWT middleware và Docker Compose, chúng ta có thể tối ưu hiệu năng, giảm chi phí vận hành và dễ dàng mở rộng trong môi trường micro‑service. Đối với những nhà phát triển muốn nhanh chóng triển khai giải pháp CMS + GraphQL cho các dự án web hoặc mobile, việc nắm vững kiến trúc trên là nền tảng vững chắc.
Để nâng cao kỹ năng thực tiễn, bạn có thể Tham khảo khóa học "Xây dựng Back-End Nodejs bằng Strapi CMS" tại đây.




