Правила дизайну: іменування та стандарти
Правила дизайну: іменування та стандарти
1. Принцип: Явне краще за неявне
Порівняйте два варіанти:
{
"time": 1708934400,
"value": 10.5,
"status": 1
}
Що таке time? Час створення? Оновлення? Доставки? value — це що? Ціна? Об'єм? Рейтинг? status: 1 — 1 означає «активний»? «Створений»? «Помилка»?
{
"created_at": "2024-02-26T14:00:00Z",
"price": "10.50",
"currency": "UAH",
"status": "processing"
}
Самодокументований JSON. Кожне поле однозначно зрозуміле без документації.
Правила явного іменування
Ім'я поля повинно однозначно описувати його зміст
// ❌ Неоднозначне
app.MapGet("/v1/orders/{id}", (int id) =>
Results.Ok(new
{
id,
time = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
value = 10.5,
type = 1
}));
// ✅ Самодокументоване
app.MapGet("/v1/orders/{id}", (int id) =>
Results.Ok(new
{
id,
created_at = "2024-02-26T14:00:00Z",
price = "10.50",
currency = "UAH",
status = "processing"
}));
Булеві поля: is_, has_, can_
{
"is_active": true,
"has_discount": false,
"can_be_cancelled": true
}
Колекції: завжди множина
{
"orders": [...],
"items": [...],
"attachments": [...]
}
Уникайте абревіатур
❌ qty, amt, desc, addr, cfg, msg
✅ quantity, amount, description, address, config, message
2. Стандарти форматів даних
2.1. Дата і час: ISO 8601
Формат: 2024-02-26T14:30:00Z
З часовою зоною: 2024-02-26T16:30:00+02:00
Тільки дата: 2024-02-26
Тільки час: 14:30:00
Тривалість: P1DT12H30M (1 день 12 годин 30 хв)
app.MapGet("/v1/orders/{id}", (int id) =>
{
var order = new
{
id,
// ISO 8601 з UTC — завжди "Z" в кінці
created_at = DateTime.UtcNow
.ToString("o"), // "2024-02-26T14:30:00.0000000Z"
// Або DateTimeOffset для зони
updated_at = DateTimeOffset.Now
.ToString("o"), // "2024-02-26T16:30:00+02:00"
// Тривалість
estimated_preparation = "PT5M30S" // 5 хв 30 сек
};
return Results.Ok(order);
});
DateTime та DateTimeOffset у формат ISO 8601. Використовуйте DateTime.UtcNow (а не DateTime.Now) для API, щоб уникнути плутанини з часовими зонами.2.2. Грошові суми: рядок або цілі числа
double або float. Через обмеження IEEE 754 (floating point), 0.1 + 0.2 = 0.30000000000000004.{
"price": 10.5,
"total": 0.30000000000000004
}
Втрата точності! У фінансових розрахунках — катастрофа.
{
"price": "10.50",
"total": "0.30",
"currency": "UAH"
}
Рядок зберігає точне представлення числа.
{
"price_minor_units": 1050,
"currency": "UAH"
}
10.50 UAH = 1050 копійок. Ціле число — ідеальна точність.
app.MapPost("/v1/orders", (OrderRequest req) =>
{
// Ціна — рядок, парсимо у decimal
// decimal має 28-29 значущих цифр
if (!decimal.TryParse(req.Price, out var price))
return Results.BadRequest(
new { error = "Invalid price format" });
// Або використовуємо мінімальні одиниці
var priceMinorUnits = (int)(price * 100);
return Results.Created("/v1/orders/42", new
{
id = 42,
price = req.Price, // "10.50" — рядок
currency = req.Currency, // "UAH"
price_minor_units = priceMinorUnits // 1050
});
});
record OrderRequest(string Price, string Currency);
2.3. Валюта: ISO 4217
Для позначення валюти використовуйте ISO 4217 — тризначний код:
| Код | Валюта |
|---|---|
UAH | Українська гривня |
USD | Долар США |
EUR | Євро |
GBP | Фунт стерлінгів |
{
"price": "10.50",
"currency": "UAH"
}
1 = UAH, 2 = USD) або скорочення (грн, $). Використовуйте тільки ISO 4217.2.4. Мова та локалізація: ISO 639 та IETF BCP 47
{
"language": "uk",
"locale": "uk-UA"
}
Заголовок Accept-Language дозволяє клієнту вказати бажану мову:
GET /v1/menu HTTP/1.1
Accept-Language: uk-UA, uk;q=0.9, en;q=0.5
2.5. Країна: ISO 3166-1 alpha-2
{
"country": "UA",
"phone_prefix": "+380"
}
3. Стандарти іменування
3.1. URL: kebab-case
✅ /v1/coffee-machines/{id}
✅ /v1/long-term-orders/{id}
❌ /v1/coffeeMachines/{id}
❌ /v1/coffee_machines/{id}
3.2. Query-параметри: snake_case
✅ /v1/orders?user_id=42&sort_by=created_at
❌ /v1/orders?userId=42&sortBy=createdAt
3.3. JSON-тіло: snake_case або camelCase
{
"user_id": 42,
"first_name": "Олексій",
"coffee_machine_id": 123
}
Перевага: легко переносити параметр з query у тіло й навпаки.
{
"userId": 42,
"firstName": "Олексій",
"coffeeMachineId": 123
}
Перевага: відповідає конвенціям JavaScript та .NET.
var builder = WebApplication.CreateBuilder(args);
// За замовчуванням ASP.NET Core
// вже використовує camelCase
builder.Services.ConfigureHttpJsonOptions(options =>
{
// Якщо потрібен snake_case:
options.SerializerOptions.PropertyNamingPolicy =
System.Text.Json.JsonNamingPolicy.SnakeCaseLower;
});
var app = builder.Build();
app.MapGet("/v1/orders/{id}", (int id) =>
Results.Ok(new OrderResponse(
id,
"lungo",
"10.50",
"UAH")));
app.Run();
// C# використовує PascalCase для властивостей
// але JSON серіалізується у обраний кейсинг
record OrderResponse(
int Id,
string Recipe,
string Price,
string Currency);
// → camelCase: {"id":1,"recipe":"lungo","price":"10.50","currency":"UAH"}
// → snake_case: {"id":1,"recipe":"lungo","price":"10.50","currency":"UAH"}
3.4. Заголовки: Train-Case
✅ Content-Type, Authorization, Accept-Language
✅ X-CoffeeAPI-Request-Id (кастомний)
Зведена таблиця
| Де | Кейсинг | Приклад |
|---|---|---|
| URL path | kebab-case | /v1/coffee-machines |
| Query params | snake_case | ?user_id=42 |
| JSON body | snake_case або camelCase | "user_id" або "userId" |
| Заголовки | Train-Case | Content-Type |
| Домен | lowercase | api.coffee-service.com |
4. Принцип консистентності
Консистентність — найважливіший принцип дизайну API. Обрали одну конвенцію — дотримуйтесь її всюди:
Одна сутність — одне ім'я
order в одному місці, не називайте його purchase, request або booking в іншому.Однакова структура відповідей
{ items: [...], total: N }, то всі списки мають повертати таку саму структуру.Однакові поля — однаковий тип
id — число в одному ендпоінті, воно має бути числом всюди. Не "id": 42 в одному місці і "id": "42" в іншому.Одна помилка — одна структура
{ error, message, details }) однаковий для всіх ендпоінтів, незалежно від типу помилки.// ✅ Консистентна «обгортка» для всіх списків
app.MapGet("/v1/orders", (int? limit, string? cursor) =>
{
var orders = db.GetOrders(limit ?? 20, cursor);
return Results.Ok(new ListResponse<Order>(
orders.Items, orders.Total, orders.NextCursor));
});
app.MapGet("/v1/recipes", (int? limit, string? cursor) =>
{
var recipes = db.GetRecipes(limit ?? 20, cursor);
return Results.Ok(new ListResponse<Recipe>(
recipes.Items, recipes.Total, recipes.NextCursor));
});
// Єдина структура для всіх списків
record ListResponse<T>(
IEnumerable<T> Items,
int Total,
string? NextCursor);
5. Стандартні поля кожного ресурсу
Кожен ресурс у вашому API повинен мати набір стандартних полів:
{
"id": 42,
"created_at": "2024-02-26T14:00:00Z",
"updated_at": "2024-02-26T16:30:00Z",
"recipe": "lungo",
"status": "processing"
}
| Поле | Тип | Опис |
|---|---|---|
id | int чи string (UUID) | Унікальний ідентифікатор |
created_at | ISO 8601 | Час створення |
updated_at | ISO 8601 | Час останнього оновлення |
deleted_at: null | ISO 8601 для soft delete. Якщо ресурс має автора — created_by: int.6. Перерахування (enum): рядки, а не числа
{
"status": 1,
"type": 3,
"priority": 2
}
Що означає status: 1? Без документації — нічого.
{
"status": "processing",
"type": "delivery",
"priority": "high"
}
Самодокументований JSON. Зрозуміло без документації.
var builder = WebApplication.CreateBuilder(args);
builder.Services.ConfigureHttpJsonOptions(options =>
{
// Серіалізувати enum як рядки
options.SerializerOptions.Converters.Add(
new System.Text.Json.Serialization
.JsonStringEnumConverter());
});
var app = builder.Build();
app.MapGet("/v1/orders/{id}", (int id) =>
Results.Ok(new
{
id,
status = OrderStatus.Processing,
priority = Priority.High
}));
// → {"id":1,"status":"Processing","priority":"High"}
app.Run();
enum OrderStatus { Created, Processing, Completed, Cancelled }
enum Priority { Low, Medium, High }
7. Практичні завдання
Рівень 1: Рефакторинг
Перепишіть JSON-відповідь, дотримуючись усіх правил іменування:
{
"ts": 1708934400,
"val": 10.5,
"curr": "uah",
"stat": 1,
"usrNm": "Олексій",
"is_deleted": 0,
"Items": ["latte", "lungo"]
}
Визначте правильний тип та формат для кожного поля:
- Дата народження
- Ціна товару
- Координати GPS (широта/довгота)
- Статус замовлення
- Кількість товарів
- Мова інтерфейсу
- Країна реєстрації
- Тривалість приготування кави
Рівень 2: Проєктування
Створіть Style Guide для вашого API — документ, що описує:
- Кейсинг для URL, query, JSON
- Формат дати, грошей, enum
- Структуру списків та помилок
- Стандартні поля кожного ресурсу
- Правила іменування (булеві поля, колекції)
Реалізуйте 3 ендпоінти (GET список, GET за id, POST створення), що повністю дотримуються вашого Style Guide.
8. Резюме
Явне > Неявне
time, value, type без контексту. Булеві — з is_/has_/can_.Стандарти форматів
Кейсинг за конвенцією
kebab-case. Query: snake_case. JSON: snake_case або camelCase (обрати одне!). Заголовки: Train-Case.Консистентність
Далі: у наступній статті ми розберемо валідацію, ліміти та обробку помилок — технічні обмеження, валідацію вхідних даних та формування інформативних повідомлень про помилки.
Номенклатура URL та CRUD-операції
Правила побудови URL ресурсів: path vs query, версіонування, кросдоменні операції. CRUD — Create, Read, Update, Delete — та чому в реальності потрібно 8-10 ендпоінтів замість чотирьох.
Валідація, ліміти та обробка помилок
Технічні обмеження API, валідація вхідних даних, структуровані помилки, RFC 9457 Problem Details, розділення помилок на клієнтські та серверні, та моніторинг.