
はじめに
Strapiは最も人気のあるオープンソースのヘッドレスCMSプラットフォームの1つですが、何百人ものコントリビューターと何千ものプルリクエストを抱える巨大なコードベースでもあります。このような大きなプロジェクトの品質を高く保つことは容易ではありません。すべてのコントリビューションが信頼でき、読みやすく、安全であり続けるためには、明確で一貫したコードレビューのルールが必要です。
この記事では、Strapiの公開リポジトリに基づいたコードレビューのルールを集めました。これらのルールは、実際の作業から生まれたものです。実際の問題、議論、プルリクエストは、コードベースを安定させながらプロジェクトの成長を助けました。
大規模なオープンソースプロジェクトでコード品質を維持するのが難しい理由
大規模なオープンソースプロジェクトで品質を維持することは、その規模と貢献の多様性から困難です。ボランティアからベテランのエンジニアまで、何百、何千もの開発者がプルリクエストを提出し、それぞれが新機能やバグ修正、リファクタを導入します。明確なルールがなければ、コードベースはすぐに一貫性がなくなったり、もろくなったり、ナビゲートが難しくなったりします。
主な課題には以下のようなものがある:
- 様々な経験レベルの多様な貢献者。
- モジュール間で一貫性のないコーディングパターン。
- 隠れたバグや重複するロジックが忍び寄る。
- プロセスが実施されない場合のセキュリティリスク。
- コードベース全体を熟知していないボランティアにとっては、時間のかかるレビュー。
これらの課題に対処するために、成功するプロジェクトは、共有された標準、自動化されたツール、明確なガイドラインといった、構造化されたプロセスに依存している。これらのプラクティスは、プロジェクトが成長し貢献者が増えても、保守性、可読性、セキュリティを保証する。
これらのルールに従うことで、保守性、セキュリティ、オンボーディングがどのように改善されるのか。
明確なコードレビューのルールを守ることは、プロジェクトの健全性に直接影響する:
- 保守性:一貫したフォルダ構造、命名規則、コーディングパターンにより、コードベースの読み取り、ナビゲート、拡張が容易になります。
- セキュリティ入力検証、サニタイズ、パーミッションチェック、制御されたデータベースアクセスが脆弱性を減らし、偶発的なデータ漏洩を防ぎます。
- オンボーディングの迅速化:共有された標準、文書化されたユーティリティ、および明確な例は、新しい貢献者がプロジェクトを迅速に理解し、自信を持って貢献するのに役立ちます。
これらのルールを適用することで、チームは、貢献者の数が増えても、コードベースの拡張性、信頼性、安全性を維持することができる。
コンテクストからルールへの橋渡し
ルールを見る前に、ストラピのようなプロジェクトでコードの品質を高く保つことは、一般的なベスト・プラクティスに従うことだけではないことを理解することが重要だ。明確なパターンと基準を持つことで、何百人ものコントリビューターが同じページにとどまることができるのです。以下の20のルールは、それぞれStrapiのコードベースに現れる実際の課題に焦点を当てている。
各規則の例は、非準拠と準拠の両方のアプローチを示しており、これらの原則が実際にどのように適用されるかを明確に示している。
では、Strapiのコードベースがスケーラブルで一貫性があり、高品質であるためのルールを、プロジェクト構造とコンフィギュレーション・スタンダードから探ってみよう。
ルールプロジェクトの構成と一貫性
1.ストラピの確立されたフォルダ規約に従う。
ファイルを散在させたり、新しい構造を考案したりしないこと。ナビゲーションを予測しやすくするために、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の定義を含めること。共有モジュールでは、任意の型、欠落した戻り値型、暗黙の型推論の使用を避けること。
非準拠の 例
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 └── ユーザー/
4 ├── コントローラー/
5 │ └── メインコントローラー.js
6 ├── サービス/
7 │ └── アカウントサービス.js
8 ├── ルート/
9 │ └── ユーザー.js✅ 準拠例
1src/
2└── api/
3 └── ユーザー/
4 ├── コントローラー/
5 │ └── ユーザー.js
6 ├── サービス/
7 │ └── ユーザー.js
8 ├── ルート/
9 │ └── ユーザー.js
10 └── コンテンツ-タイプ/
11 └── ユーザー/スキーマ.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
モジュール.exports = (テキスト) =>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.本番前にデバッグログを削除する
ログが環境設定を尊重し、機密情報を公開しないようにするために、常に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.ストラピのクエリーエンジンを一貫して使う
保守性、可読性、予測可能な動作を保証するために、単一の一貫したクエリー・アプローチを使用する。
非準拠の 例
// 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.データベース呼び出しを最適化する
パフォーマンスのボトルネックを防ぎ、不要な連続呼び出しを減らすために、関連するデータベースクエリをバッチ処理したり、1つの操作にまとめたりします。
非準拠の 例
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.ストラピバリデータで入力を検証する
コントローラ、サービス、データベース操作で使用する前に、一貫した検証メカニズムを使用して、すべての受信データを検証します。
非準拠の 例
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.ブームによる一貫したエラー処理
集中的または統一的なエラー処理メカニズムを使用して、すべての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.各機能のテストを追加または更新する。
テストのない新しいコードはマージされない。テストはdoneの定義の一部だ。
非準拠の 例
// 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のコードベースから直接取り入れた実践的な教訓であり、プロジェクトをより保守しやすく、より安全で、より読みやすくするものです。
もしあなたのプロジェクトが成長しているなら、これらの教訓をコードレビューに活かしてください。混乱したコードの後始末に費やす時間を減らし、本当に重要な機能を構築する時間を増やすのに役立つだろう。
.avif)
