
はじめに
Strapiは、最も人気のあるオープンソースのヘッドレスCMSプラットフォームの一つですが、数百人の貢献者と数千のプルリクエストを抱える巨大なコードベースでもあります。このような大規模プロジェクトの品質を高く維持するのは容易ではありません。すべての貢献が信頼性、可読性、セキュリティを保つためには、明確で一貫したコードレビューのルールが必要です。
本記事では、Strapiの公開リポジトリに基づいたコードレビューのルールをまとめました。これらのルールは、実際の課題、議論、プルリクエストといった実務から生まれたものであり、コードベースの安定性を保ちながらプロジェクトの成長を助けてきました。
大規模なオープンソースプロジェクトでコード品質を維持することが難しい理由
大規模なオープンソースプロジェクトで品質を維持することは、貢献の規模と多様性のため困難です。ボランティアから経験豊富なエンジニアまで、数百人、あるいは数千人もの開発者がプルリクエストを提出し、それぞれが新機能、バグ修正、またはリファクタリングを導入します。明確なルールがなければ、コードベースはすぐに一貫性がなくなり、脆くなり、ナビゲートが困難になる可能性があります。
主な課題には以下が含まれます。
- 多様な貢献者: さまざまな経験レベルを持つ貢献者。
- モジュール間での一貫性のないコーディングパターン。
- Hidden bugs and duplicated logic が忍び寄ります。
- セキュリティリスク:プロセスが強制されない場合。
- 時間のかかるレビュー(コードベース全体に不慣れなボランティア向け)。
これらの課題に対処するため、成功するプロジェクトは、共有された標準、自動化されたツール、明確なガイドラインといった構造化されたプロセスに依存します。これらの実践は、プロジェクトが成長し、より多くの貢献者を引き付けるにつれても、保守性、可読性、およびセキュリティを保証します。
これらのルールに従うことで、保守性、セキュリティ、オンボーディングがどのように改善されるか
明確なコードレビュー規則のセットを遵守することは、プロジェクトの健全性に直接的な影響を与えます:
- 保守性: 一貫したフォルダ構造、命名規則、コーディングパターンにより、コードベースの読み取り、ナビゲート、拡張が容易になります。
- セキュリティ: 入力検証、サニタイズ、権限チェック、および制御されたデータベースアクセスにより、脆弱性を低減し、偶発的なデータ漏洩を防止します。
- オンボーディングの迅速化: 共有された標準、文書化されたユーティリティ、および明確な例は、新しい貢献者がプロジェクトを迅速に理解し、自信を持って貢献するのに役立ちます。
これらのルールを適用することで、貢献者の数が増加しても、チームはコードベースがスケーラブルで、信頼性が高く、安全な状態を維持できることを確実にできます。
コンテキストとルールを連携させる
ルールを見る前に、Strapiのようなプロジェクトでコード品質を高く保つことは、一般的なベストプラクティスに従うことだけではないことを理解することが重要です。何百人もの貢献者が共通認識を持つことを助ける、明確なパターンと標準を持つことが重要です。以下の20のルールのそれぞれは、Strapiのコードベースに現れる実際の課題に焦点を当てています。
各ルールに提供されている例は、非準拠アプローチと準拠アプローチの両方を示しており、これらの原則が実際にどのように適用されるかを明確に示しています。
では、プロジェクト構造と構成標準から始めて、Strapiのコードベースをスケーラブルで一貫性があり、高品質にするためのルールを探っていきましょう。
ルール: プロジェクト構造と一貫性
1. Strapiの確立されたフォルダー規約に従ってください
ファイルを散乱させたり、新しい構造を発明したりすることは避けてください。ナビゲーションを予測可能に保つため、Strapiの確立されたプロジェクトレイアウトに従ってください。
❌ 非準拠例
1src/
2├── controllers/
3│ └── userController.js
4├── services/
5│ └── userLogic.js
6├── routes/
7│ └── userRoutes.js
8└── utils/
9 └── helper.js✅ 準拠例
1src/
2└── API/
3 └── user/
4 ├── controllers/
5 │ └── user.js
6 ├── services/
7 │ └── user.js
8 ├── routes/
9 │ └── user.js
10 └── content-types/
11 └── user/schema.json2. 設定ファイルの一貫性を保つ
一貫性を確保し、エラーを防ぐため、すべての設定ファイルで同じ構造、命名、および書式設定の規則を使用してください。
❌ 非準拠例
1// config/server.js
2module.exports = {
3 PORT: 1337,
4 host: '0.0.0.0',
5 APP_NAME: 'my-app'
6}
7
8// config/database.js
9export default {
10 connection: {
11 client: 'sqlite',
12 connection: { filename: '.tmp/data.db' }
13 }
14}
15
16// config/plugins.js
17module.exports = ({ env }) => ({
18 upload: { provider: "local" },
19 email: { provider: 'sendgrid' }
20});✅ 準拠例
1// config/server.js
2module.exports = ({ env }) => ({
3 host: env('HOST', '0.0.0.0'),
4 port: env.int('PORT', 1337),
5 app: { keys: env.array('APP_KEYS') },
6});
7
8// config/database.js
9module.exports = ({ env }) => ({
10 connection: {
11 client: 'sqlite',
12 connection: { filename: env('DATABASE_FILENAME', '.tmp/data.db') },
13 useNullAsDefault: true,
14 },
15});
16
17// config/plugins.js
18module.exports = ({ env }) => ({
19 upload: { provider: 'local' },
20 email: { provider: 'sendgrid' },
21});3. 厳格な型安全性を維持します
すべての新規または更新されたコードには、正確なTypeScript型またはJSDoc定義を含める必要があります。共有モジュールでは、any型の使用、戻り値型の欠落、暗黙的な型推論を避けてください。
❌ 非準拠例
1// src/api/user/services/user.ts
2export const createUser = (data) => {
3 return strapi.db.query('api::user.user').create({ data });
4};✅ 準拠例
1// src/api/user/services/user.ts
2import { User } from './types';
3
4export const createUser = async (data: User): Promise<User> => {
5 return await strapi.db.query('api::user.user').create({ data });
6};4. サービスとコントローラーの一貫した命名を行います
コントローラー名とサービス名は、そのドメインと明確に一致する必要があります (例: user.controller.jsとuser.service.js)。
❌ 非準拠例
1src/
2└── API/
3 └── user/
4 ├── controllers/
5 │ └── mainController.js
6 ├── services/
7 │ └── accountService.js
8 ├── routes/
9 │ └── user.js✅ 準拠例
1src/
2└── API/
3 └── user/
4 ├── controllers/
5 │ └── user.js
6 ├── services/
7 │ └── user.js
8 ├── routes/
9 │ └── user.js
10 └── content-types/
11 └── user/schema.json
ルール: コード品質と保守性
5. 早期リターンで制御フローを簡素化します
深いif/elseのネストの代わりに、条件が失敗した場合は早期にリターンします。
❌ 非準拠例
1// src/api/article/controllers/article.js
2module.exports = {
3 async create(ctx) {
4 const { title, content, author } = ctx.request.body;
5
6 if (title) {
7 if (content) {
8 if (author) {
9 const article = await strapi.db.query('api::article.article').create({
10 data: { title, content, author },
11 });
12 ctx.body = article;
13 } else {
14 ctx.throw(400, 'Missing author');
15 }
16 } else {
17 ctx.throw(400, 'Missing content');
18 }
19 } else {
20 ctx.throw(400, 'Missing title');
21 }
22 },
23};✅ 準拠例
1// src/api/article/controllers/article.js
2module.exports = {
3 async create(ctx) {
4 const { title, content, author } = ctx.request.body;
5
6 if (!title) ctx.throw(400, 'Missing title');
7 if (!content) ctx.throw(400, 'Missing content');
8 if (!author) ctx.throw(400, 'Missing author');
9
10 const article = await strapi.db.query('api::article.article').create({
11 data: { title, content, author },
12 });
13
14 ctx.body = article;
15 },
16};6. コントローラー内での過度なネストを避ける
コントローラーやサービス内に大きなネストされたロジックブロックを避けてください。繰り返される条件や複雑な条件は、適切に命名されたヘルパー関数やユーティリティに抽出してください。
❌ 非準拠例
1// src/api/order/controllers/order.js
2module.exports = {
3 async create(ctx) {
4 const { items, user } = ctx.request.body;
5
6 if (user && user.role === 'customer') {
7 if (items && items.length > 0) {
8 const stock = await strapi.service('api::inventory.inventory').checkStock(items);
9 if (stock.every((i) => i.available)) {
10 const order = await strapi.db.query('api::order.order').create({ data: { items, user } });
11 ctx.body = order;
12 } else {
13 ctx.throw(400, 'Some items are out of stock');
14 }
15 } else {
16 ctx.throw(400, 'No items in order');
17 }
18 } else {
19 ctx.throw(403, 'Unauthorized user');
20 }
21 },
22};✅ 準拠例
1// src/api/order/utils/validation.js
2const isCustomer = (user) => user?.role === 'customer';
3const hasItems = (items) => Array.isArray(items) && items.length > 0;
4
5// src/api/order/controllers/order.js
6module.exports = {
7 async create(ctx) {
8 const { items, user } = ctx.request.body;
9
10 if (!isCustomer(user)) ctx.throw(403, 'Unauthorized user');
11 if (!hasItems(items)) ctx.throw(400, 'No items in order');
12
13 const stock = await strapi.service('api::inventory.inventory').checkStock(items);
14 const allAvailable = stock.every((i) => i.available);
15 if (!allAvailable) ctx.throw(400, 'Some items are out of stock');
16
17 const order = await strapi.db.query('api::order.order').create({ data: { items, user } });
18 ctx.body = order;
19 },
20};7. ビジネスロジックをコントローラーから分離する
コントローラーは薄く保ち、リクエストのオーケストレーションのみを行うべきです。ビジネスロジックはサービスに移動してください。
❌ 非準拠例
1// src/api/article/controllers/article.js
2module.exports = {
3 async create(ctx) {
4 const { title, content, authorId } = ctx.request.body;
5
6 const author = await strapi.db.query('api::author.author').findOne({ where: { id: authorId } });
7 if (!author) ctx.throw(400, 'Author not found');
8
9 const timestamp = new Date().toISOString();
10 const slug = title.toLowerCase().replace(/\s+/g, '-');
11
12 const article = await strapi.db.query('api::article.article').create({
13 data: { title, content, slug, publishedAt: timestamp, author },
14 });
15
16 await strapi.plugins['email'].services.email.send({
17 to: author.email,
18 subject: `New article: ${title}`,
19 html: `<p>${content}</p>`,
20 });
21
22 ctx.body = article;
23 },
24};✅ 準拠例
1// src/api/article/controllers/article.js
2module.exports = {
3 async create(ctx) {
4 const article = await strapi.service('api::article.article').createArticle(ctx.request.body);
5 ctx.body = article;
6 },
7};// src/api/article/services/article.js
module.exports = ({ strapi }) => ({
async createArticle(data) {
const { title, content, authorId } = data;
const author = await strapi.db.query('api::author.author').findOne({ where: { id: authorId } });
if (!author) throw new Error('Author not found');
const slug = title.toLowerCase().replace(/\s+/g, '-');
const article = await strapi.db.query('api::article.article').create({
data: { title, content, slug, author },
});
await strapi.plugins['email'].services.email.send({
to: author.email,
subject: `New article: ${title}`,
html: `<p>${content}</p>`,
});
return article;
},
});8. 繰り返しパターンにはユーティリティ関数を使用する
重複するパターン(例:バリデーション、フォーマット)は共有ユーティリティに集約すべきです。
❌ 非準拠例
// src/api/article/controllers/article.js
module.exports = {
async create(ctx) {
const { title } = ctx.request.body;
const slug = title.toLowerCase().replace(/\s+/g, '-');
ctx.body = await strapi.db.query('api::article.article').create({ data: { ...ctx.request.body, slug } });
},
};
// src/api/event/controllers/event.js
module.exports = {
async create(ctx) {
const { name } = ctx.request.body;
const slug = name.toLowerCase().replace(/\s+/g, '-');
ctx.body = await strapi.db.query('api::event.event').create({ data: { ...ctx.request.body, slug } });
},
};✅ 準拠例
// src/utils/slugify.js
module.exports = (text) => text.toLowerCase().trim().replace(/\s+/g, '-');// src/api/article/controllers/article.js
const slugify = require('../../../utils/slugify');
module.exports = {
async create(ctx) {
const { title } = ctx.request.body;
const slug = slugify(title);
ctx.body = await strapi.db.query('api::article.article').create({ data: { ...ctx.request.body, slug } });
},
};9. 本番稼働前にデバッグログを削除する
本番コードでconsole.log、console.warn、またはconsole.errorを使用しないでください。常にstrapi.logまたは設定されたロガーを使用して、ログが環境設定を尊重し、機密情報の露出を避けるようにしてください。
❌ 非準拠例
// src/api/user/controllers/user.js
module.exports = {
async find(ctx) {
console.log('Request received:', ctx.request.body); // Unsafe in production
const users = await strapi.db.query('api::user.user').findMany();
console.log('Users fetched:', users.length);
ctx.body = users;
},
};✅ 準拠例
// src/api/user/controllers/user.js
module.exports = {
async find(ctx) {
strapi.log.info(`Fetching users for request from ${ctx.state.user?.email || 'anonymous'}`);
const users = await strapi.db.query('api::user.user').findMany();
strapi.log.debug(`Number of users fetched: ${users.length}`);
ctx.body = users;
},
};if (process.env.NODE_ENV === 'development') {
strapi.log.debug('Request body:', ctx.request.body);
}
ルール: データベースとクエリのプラクティス
10. 生のSQLクエリを避けてください
コントローラーやサービスで生のSQLクエリを実行しないでください。常に一貫した高レベルのクエリメソッド(ORMやクエリビルダーなど)を使用して、保守性を確保し、ルール/フックを適用し、セキュリティリスクを低減してください。
❌ 非準拠例
// src/api/user/services/user.js
module.exports = {
async findActiveUsers() {
const knex = strapi.db.connection;
const result = await knex.raw('SELECT * FROM users WHERE active = true'); // Raw SQL
return result.rows;
},
};✅ 準拠例
// src/api/user/services/user.js
module.exports = {
async findActiveUsers() {
return await strapi.db.query('api::user.user').findMany({
where: { active: true },
});
},
};11. Strapiのクエリエンジンを一貫して使用する
同じ機能内で異なるデータベースアクセス方法(例:ORM呼び出しと生のクエリ)を混在させないでください。単一の一貫したクエリ手法を使用して、保守性、可読性、および予測可能な動作を確保してください。
❌ 非準拠例
// src/api/order/services/order.js
module.exports = {
async getPendingOrders() {
// Using entityService
const orders = await strapi.entityService.findMany('api::order.order', {
filters: { status: 'pending' },
});
// Mixing with raw db query
const rawOrders = await strapi.db.connection.raw('SELECT * FROM orders WHERE status = "pending"');
return { orders, rawOrders };
},
};✅ 準拠例
// src/api/order/services/order.js
module.exports = {
async getPendingOrders() {
return await strapi.db.query('api::order.order').findMany({
where: { status: 'pending' },
});
},
};12. データベース呼び出しを最適化する
パフォーマンスのボトルネックを防ぎ、不要なシーケンシャルコールを削減するために、関連するデータベースクエリをバッチ処理するか、単一の操作に結合します。
❌ 非準拠例
async function getArticlesWithAuthors() {
const articles = await db.query('articles').findMany();
// Fetch author for each article sequentially
for (const article of articles) {
article.author = await db.query('authors').findOne({ id: article.authorId });
}
return articles;
}✅ 準拠例
async function getArticlesWithAuthors() {
return await db.query('articles').findMany({ populate: ['author'] });
}
ルール:APIとセキュリティ
13. Strapiバリデーターで入力を検証する
クライアントまたは外部ソースからの入力を決して信頼しないでください。コントローラー、サービス、またはデータベース操作で使用する前に、一貫した検証メカニズムを使用してすべての受信データを検証してください。
❌ 非準拠例
async function createUser(req, res) {
const { username, email } = req.body;
// Directly inserting into database without validation
const user = await db.query('users').create({ username, email });
res.send(user);
}✅ 準拠例
const Joi = require('joi');
async function createUser(req, res) {
const schema = Joi.object({
username: Joi.string().min(3).required(),
email: Joi.string().email().required(),
});
const { error, value } = schema.validate(req.body);
if (error) return res.status(400).send(error.details);
const user = await db.query('users').create(value);
res.send(user);
}14. 保存前にユーザー入力をサニタイズする
データベースに保存する前、または他のシステムに渡す前に、すべての入力をサニタイズしてください。
❌ 非準拠例
async function createComment(req, res) {
const { text, postId } = req.body;
// Directly saving data
const comment = await db.query('comments').create({ text, postId });
res.send(comment);
}✅ 準拠例
const sanitizeHtml = require('sanitize-html');
async function createComment(req, res) {
const { text, postId } = req.body;
const sanitizedText = sanitizeHtml(text, { allowedTags: [], allowedAttributes: {} });
const comment = await db.query('comments').create({ text: sanitizedText, postId });
res.send(comment);
}15. 権限チェックを強制する
保護されたすべてのルートに権限チェックを適用し、認可されたユーザーのみがアクセスできるようにします。
❌ 非準拠例
async function deleteUser(req, res) {
const { userId } = req.params;
// No check for admin or owner
await db.query('users').delete({ id: userId });
res.send({ success: true });
}✅ 準拠例
async function deleteUser(req, res) {
const { userId } = req.params;
const requestingUser = req.user;
// Allow only admins or the owner
if (!requestingUser.isAdmin && requestingUser.id !== userId) {
return res.status(403).send({ error: 'Forbidden' });
}
await db.query('users').delete({ id: userId });
res.send({ success: true });
}16. Boomによる一貫したエラーハンドリング
一元化された、または統一されたエラー処理メカニズムを使用して、すべてのAPIルートでエラーを一貫して処理します。
❌ 非準拠例
async function getUser(req, res) {
const { id } = req.params;
try {
const user = await db.query('users').findOne({ id });
if (!user) res.status(404).send('User not found'); // raw string error
else res.send(user);
} catch (err) {
res.status(500).send(err.message); // different error format
}
}✅ 準拠例
const { createError } = require('../utils/errors');
async function getUser(req, res, next) {
try {
const { id } = req.params;
const user = await db.query('users').findOne({ id });
if (!user) throw createError(404, 'User not found');
res.send(user);
} catch (err) {
next(err); // passes error to centralized error handler
}
}// src/utils/errors.js
function createError(status, message) {
return { status, message };
}
function errorHandler(err, req, res, next) {
res.status(err.status || 500).json({ error: err.message });
}
module.exports = { createError, errorHandler };
ルール: テストとドキュメント
17. すべての機能に対してテストを追加または更新する
テストのない新しいコードはマージされません。テストは「完了の定義」の一部です。
❌ 非準拠例
// src/api/user/services/user.js
module.exports = {
async createUser(data) {
const user = await db.query('users').create(data);
return user;
},
};
// No test file exists for this service✅ 準拠例
// tests/user.service.test.js
const { createUser } = require('../../src/api/user/services/user');
describe('User Service', () => {
it('should create a new user', async () => {
const mockData = { username: 'testuser', email: 'test@example.com' };
const result = await createUser(mockData);
expect(result).toHaveProperty('id');
expect(result.username).toBe('testuser');
expect(result.email).toBe('test@example.com');
});
});18. 新しいエンドポイントを文書化する
すべてのAPI追加は、マージ前にリファレンスドキュメントに文書化される必要があります。
❌ 非準拠例
// src/api/user/controllers/user.js
module.exports = {
async deactivate(ctx) {
const { userId } = ctx.request.body;
await db.query('users').update({ id: userId, active: false });
ctx.body = { success: true };
},
};
// No update in API reference or docs✅ 準拠例
// src/api/user/controllers/user.js
module.exports = {
/**
* Deactivate a user account.
* POST /users/deactivate
* Body: { userId: string }
* Response: { success: boolean }
* Errors: 400 if userId missing, 404 if user not found
*/
async deactivate(ctx) {
const { userId } = ctx.request.body;
if (!userId) ctx.throw(400, 'userId is required');
const user = await db.query('users').findOne({ id: userId });
if (!user) ctx.throw(404, 'User not found');
await db.query('users').update({ id: userId, active: false });
ctx.body = { success: true };
},
};リファレンスドキュメント更新例:
### POST /users/deactivate
**Request Body:**
```json
{
"userId": "string"
}レスポンス:
{
"success": true
}エラー:
- 400: userIdは必須です
- 404: ユーザーが見つかりません
これが機能する理由:
- 開発者とAPIコンシューマーは、エンドポイントを確実に発見し、使用できます。
- 実装とドキュメント間の一貫性を確保します。
- メンテナンスとオンボーディングを容易にします。
---
次に、同じ形式で**ルール #19(「共有ユーティリティにJSDocを使用する」)**を続けますか?19. 共有ユーティリティにJSDocを使用する
共有関数は、オンボーディングとコラボレーションを容易にするためにJSDocで説明されるべきです。
❌ 非準拠例
// src/utils/slugify.js
function slugify(text) {
return text.toLowerCase().trim().replace(/\s+/g, '-');
}
module.exports = slugify;✅ 準拠例
// src/utils/slugify.js
/**
* Converts a string into a URL-friendly slug.
*
* @param {string} text - The input string to convert.
* @returns {string} A lowercased, trimmed, dash-separated slug.
*/
function slugify(text) {
return text.toLowerCase().trim().replace(/\s+/g, '-');
}
module.exports = slugify;20. 重要なPRごとに変更履歴を更新する
PRをマージする前に、プロジェクトの変更ログをすべての重要な機能、バグ修正、またはAPI変更で更新してください。
❌ 非準拠例
# CHANGELOG.md
## [1.0.0] - 2025-09-01
- 初回リリース✅ 準拠例
# CHANGELOG.md
## [1.1.0] - 2025-10-06
- ユーザー無効化エンドポイント(`POST /users/deactivate`)を追加
- 記事タイトルのスラッグ生成バグを修正
- バッチ送信を処理するようにメール通知サービスを更新まとめ
当社はStrapiの公開リポジトリを調査し、一貫したコードパターンが大規模なオープンソースプロジェクトの品質を損なうことなく成長を助ける方法を理解しました。これら20のルールは理論ではありません。Strapiのコードベースから直接得られた実践的な教訓であり、プロジェクトの保守性、セキュリティ、可読性を向上させます。
プロジェクトが成長している場合、これらの教訓をコードレビューに適用してください。これにより、煩雑なコードのクリーンアップに費やす時間を減らし、真に重要な機能の構築により多くの時間を費やすことができます。

