- Giới thiệu
- 1. Cấu trúc thư mục đề xuất
- 2. Thiết lập Yarn workspaces cho frontend
- 3. Thiết lập Composer path repository cho Laravel
- 4. Chia sẻ validation giữa Laravel và Next.js
- 5. Cấu hình Docker Compose cho môi trường phát triển
- 6. Xây dựng API chung và tích hợp với Next.js
- 7. CI/CD thống nhất với GitHub Actions
- 8. Lợi ích và nhược điểm của kiến trúc monorepo
- 9. Kinh nghiệm thực tiễn khi áp dụng monorepo
- Kết luận
Giới thiệu
Trong môi trường phát triển full‑stack ngày nay, việc tách biệt hoàn toàn backend (Laravel) và frontend (Next.js) thường dẫn đến việc quản lý phụ thuộc, đồng bộ dữ liệu và quy trình CI/CD trở nên phức tạp. Monorepo – một kho mã duy nhất chứa cả hai phần – giúp giải quyết những vấn đề này bằng cách:
- Đồng bộ phiên bản thư viện chung (ví dụ:
axios,zod). - Chia sẻ các schema, validation và kiểu dữ liệu giữa server và client.
- Triển khai pipeline CI/CD thống nhất, giảm chi phí bảo trì.
Bài viết sẽ đi sâu vào cách thiết lập monorepo cho Laravel và Next.js, cách chia sẻ mã nguồn, cấu hình Docker Compose và xây dựng pipeline CI/CD bằng GitHub Actions. Tất cả đều được minh họa bằng các đoạn code thực tế.
1. Cấu trúc thư mục đề xuất
Một cấu trúc đơn giản nhưng hiệu quả như sau:
my-project/ ├─ backend/ # Laravel application │ ├─ app/ │ ├─ bootstrap/ │ └─ ... ├─ frontend/ # Next.js application │ ├─ pages/ │ ├─ components/ │ └─ ... ├─ packages/ # Thư viện chia sẻ (shared) │ ├─ types/ # TypeScript types, interfaces │ ├─ validators/ # Zod schemas, Laravel FormRequests │ └─ utils/ # Helper functions dùng chung ├─ docker-compose.yml ├─ .github/workflows/ │ └─ ci.yml # CI/CD pipeline └─ README.md
Thư mục packages chứa các module có thể được import bởi cả Laravel và Next.js. Để làm được điều này, chúng ta sẽ sử dụng Yarn workspaces cho phần frontend và Composer path repository cho phần backend.
2. Thiết lập Yarn workspaces cho frontend
Trong thư mục gốc, tạo file package.json với cấu hình workspaces:
{
"private": true,
"workspaces": [
"frontend",
"packages/*"
]
}
Tiếp theo, trong frontend/package.json thêm dependency tới các package chia sẻ:
{
"name": "frontend",
"version": "1.0.0",
"dependencies": {
"@my-project/types": "*",
"@my-project/validators": "*",
"react": "^18.2.0",
"next": "^13.4.0"
}
}
Yarn sẽ tự động tạo symlink tới các thư mục trong packages, cho phép import như sau:
import { User } from '@my-project/types';
import { userSchema } from '@my-project/validators';
3. Thiết lập Composer path repository cho Laravel
Trong backend/composer.json khai báo repository dạng path:
{
"repositories": [
{
"type": "path",
"url": "../packages/*",
"options": {"symlink": true}
}
],
"require": {
"my-project/types": "*",
"my-project/validators": "*",
"laravel/framework": "^10.0"
},
"autoload": {
"psr-4": {
"App\\": "app/",
"MyProject\\Types\\": "../packages/types/src",
"MyProject\\Validators\\": "../packages/validators/src"
}
}
}
Sau khi chạy composer update, các package sẽ được symlink vào vendor, cho phép Laravel sử dụng chúng như các package thông thường.
4. Chia sẻ validation giữa Laravel và Next.js
Ví dụ: một schema người dùng cần đồng nhất validation ở cả backend và frontend. Chúng ta sẽ dùng zod cho JavaScript và illuminate/validation cho PHP, nhưng để tránh viết lại, chúng ta tạo một file JSON schema trong packages/validators và generate code cho cả hai môi trường.
4.1. Định nghĩa JSON schema
{
"$id": "User",
"type": "object",
"properties": {
"name": {"type": "string", "minLength": 2, "maxLength": 50},
"email": {"type": "string", "format": "email"},
"age": {"type": "integer", "minimum": 18}
},
"required": ["name", "email"]
}
4.2. Tạo validator cho Next.js
import { z } from 'zod';
import schema from '../schemas/user.json';
export const userSchema = z.object({
name: z.string().min(schema.properties.name.minLength).max(schema.properties.name.maxLength),
email: z.string().email(),
age: z.number().int().min(schema.properties.age.minimum)
});
4.3. Tạo FormRequest cho Laravel
'required|string|min:2|max:50',
'email' => 'required|email',
'age' => 'nullable|integer|min:18',
];
}
}
?>
Nhờ việc đặt schema trong packages/validators, cả hai dự án đều có thể import và sử dụng mà không cần sao chép lại.
5. Cấu hình Docker Compose cho môi trường phát triển
Docker Compose giúp khởi chạy đồng thời các service Laravel, Next.js, MySQL và Redis. Dưới đây là file docker-compose.yml tối giản:
version: '3.8'
services:
php:
build:
context: ./backend
dockerfile: Dockerfile
volumes:
- ./backend:/var/www/html
- ./packages:/var/www/packages
environment:
- APP_ENV=local
- DB_HOST=mysql
depends_on:
- mysql
- redis
node:
image: node:20-alpine
working_dir: /app
volumes:
- ./frontend:/app
- ./packages:/app/packages
command: sh -c "yarn install && yarn dev"
ports:
- "3000:3000"
depends_on:
- php
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: secret
MYSQL_DATABASE: mydb
ports:
- "3306:3306"
volumes:
- mysql-data:/var/lib/mysql
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
mysql-data:
Chú ý:
- Thư mục
packagesđược mount vào cả container PHP và Node, cho phép truy cập đồng thời. - Laravel sử dụng
APP_ENV=localđể bậtLaravel Sanctumcho việc xác thực API.
6. Xây dựng API chung và tích hợp với Next.js
Laravel sẽ cung cấp các endpoint RESTful, còn Next.js sẽ gọi chúng thông qua axios. Để tránh CORS, chúng ta cấu hình Sanctum với stateful domains.
6.1. Cấu hình Sanctum trong Laravel
// config/sanctum.php
return [
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', 'localhost,127.0.0.1')),
'guard' => ['web'],
];
6.2. Định nghĩa route API
group(function () {
Route::get('/user', [UserController::class, 'me']);
Route::post('/user', [UserController::class, 'store']);
});
?>
6.3. Gọi API từ Next.js (SSR)
import axios from 'axios';
import { userSchema } from '@my-project/validators';
export async function getServerSideProps(context) {
const { data } = await axios.get('http://localhost:8000/api/user', {
withCredentials: true,
headers: { Cookie: context.req.headers.cookie || '' }
});
// Validate dữ liệu trả về
const result = userSchema.safeParse(data);
if (!result.success) {
return { notFound: true };
}
return { props: { user: result.data } };
}
Kết hợp userSchema giúp đảm bảo dữ liệu nhận được luôn hợp lệ, giảm lỗi runtime.
7. CI/CD thống nhất với GitHub Actions
Một workflow duy nhất có thể:
- Chạy lint và test cho cả PHP và JavaScript.
- Build Docker images.
- Đẩy images lên Docker Hub hoặc GitHub Packages.
- Triển khai lên môi trường staging/production bằng Docker Compose.
Dưới đây là file .github/workflows/ci.yml:
name: CI/CD
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: secret
MYSQL_DATABASE: testdb
ports: [3306:3306]
options: --health-cmd "mysqladmin ping" --health-interval 10s --health-timeout 5s --health-retries 3
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
cache: 'yarn'
- name: Install frontend dependencies
run: |
yarn install --frozen-lockfile
- name: Lint & Test Frontend
run: |
yarn lint
yarn test
- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
extensions: mbstring, intl, pdo_mysql
coverage: none
- name: Install backend dependencies
working-directory: ./backend
run: composer install --no-interaction --prefer-dist --optimize-autoloader
- name: Run Laravel tests
working-directory: ./backend
env:
DB_CONNECTION: mysql
DB_HOST: 127.0.0.1
DB_DATABASE: testdb
DB_USERNAME: root
DB_PASSWORD: secret
run: |
php artisan migrate --force
php artisan test
- name: Build Docker images
run: |
docker compose -f docker-compose.yml build
- name: Push images to Docker Hub
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
run: |
echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
docker push $DOCKER_USERNAME/my-project-php:latest
docker push $DOCKER_USERNAME/my-project-node:latest
- name: Deploy to staging
if: github.ref == 'refs/heads/main'
run: |
ssh -o StrictHostKeyChecking=no ${{ secrets.STAGING_SSH }} "cd /var/www/my-project && docker compose pull && docker compose up -d"
Workflow này đảm bảo mọi thay đổi đều được kiểm tra toàn diện trước khi đưa lên môi trường thực tế.
8. Lợi ích và nhược điểm của kiến trúc monorepo
Ưu điểm
- Quản lý phụ thuộc thống nhất: Khi một package thay đổi, cả backend và frontend đều nhận được cập nhật ngay lập tức.
- CI/CD đơn giản: Một pipeline duy nhất có thể kiểm tra toàn bộ dự án.
- Chia sẻ logic: Validation, type definitions, và utility functions không cần viết lại.
- Giảm overhead vận hành: Chỉ cần duy trì một repository, một Docker Compose file.
Nhược điểm
- Thời gian clone lớn: Dự án có thể lên vài trăm MB, gây khó khăn cho các contributor mới.
- Quản lý versioning phức tạp: Khi một package thay đổi, cần cân nhắc ảnh hưởng tới cả hai phần.
- Yêu cầu cấu hình môi trường phức tạp: Đặc biệt khi triển khai micro‑service độc lập.
Đánh giá lợi‑nhược điểm giúp các team quyết định có nên áp dụng monorepo hay không dựa trên quy mô và nhu cầu thực tế.
9. Kinh nghiệm thực tiễn khi áp dụng monorepo
Dưới đây là một số tip mà chúng tôi đã rút ra trong quá trình triển khai dự án thực tế:
- Sử dụng Git submodules cho các package lớn nếu chúng không cần cập nhật thường xuyên.
- Đặt version cho mỗi package trong
packagesbằngsemantic-releaseđể tự động bump version. - Cache Docker layers trong CI để giảm thời gian build.
- Thiết lập lint rules chung (ESLint + PHP_CodeSniffer) để duy trì chất lượng code đồng nhất.
- Giám sát kích thước bundle của Next.js bằng
next-bundle-analyzerđể tránh tăng quá mức khi chia sẻ thư viện.
Kết luận
Monorepo Laravel + Next.js không chỉ giúp đồng bộ logic và giảm chi phí bảo trì, mà còn tạo nền tảng vững chắc cho CI/CD toàn diện. Khi áp dụng đúng cách, đội ngũ phát triển có thể tập trung vào việc xây dựng tính năng thay vì quản lý cấu trúc dự án.
Để nâng cao kỹ năng và có một lộ trình học tập bài bản, bạn có thể Tham khảo khóa học "Xây dựng ứng dụng kết hợp Laravel - ReactJS - NextJS" tại đây.






