UI POM vs API "POM": Why Structure Gets Confusing When Combined¶
Understanding why users struggle to see clear POM structure when mixing UI and API testing
🌍 ENGLISH VERSION¶
1️⃣ First: What is POM - and how does it apply to UI & API?¶
📌 POM (Page Object Model) - The Essence
POM = Separating "operation logic" from "test logic"
For UI: - Page = 1 web page - Page Object contains: - locators - actions (click, fill, submit...)
For API: - No "page" exists - Equivalent thinking: - Service / Client Object Model - Many teams still call it "API POM" for simplicity
2️⃣ STANDARD - CLEAR UI POM Structure¶
📁 UI Layer (UI only, no API, no assertions)
src/ui/
├── pages/
│ ├── BasePage.ts
│ ├── HomePage.ts
│ └── BasicFormPage.ts
│
└── components/ (optional – higher level)
└── Header.ts
🧩 BasePage.ts (UI foundation)
import { Page } from 'playwright';
export abstract class BasePage {
protected page: Page;
constructor(page: Page) {
this.page = page;
}
async open(path: string) {
await this.page.goto(path);
}
}
🧩 HomePage.ts (True POM)
import { BasePage } from './BasePage';
export class HomePage extends BasePage {
private basicFormLink = this.page.getByRole('link', {
name: 'Basic HTML Form Example'
});
async goToBasicForm() {
await this.basicFormLink.click();
}
}
🧠 Golden Rule of UI POM
- ❌ No expect/assertions
- ❌ No test logic
- ✅ Only locator + action
🧩 BasicFormPage.ts
export class BasicFormPage extends BasePage {
private usernameInput = this.page.locator('#username');
private submitBtn = this.page.getByRole('button', { name: 'Submit' });
async fillUsername(name: string) {
await this.usernameInput.fill(name);
}
async submit() {
await this.submitBtn.click();
}
}
3️⃣ API "POM" – Right Thinking for Beginners¶
API isn't a Page, so we rename it for accuracy.
📁 API Layer (Service / Client)
src/api/
├── clients/
│ └── user.api.ts
│
├── models/
│ └── user.model.ts
│
└── helpers/
└── apiClient.ts
🧩 apiClient.ts (base for API)
import axios from 'axios';
import { ENV } from '../../utils/env';
export const apiClient = axios.create({
baseURL: ENV.apiUrl,
timeout: 5000,
});
🧩 user.api.ts (API POM)
import { apiClient } from '../helpers/apiClient';
export class UserApi {
async createUser(payload: any) {
return apiClient.post('/users', payload);
}
async getUser(id: string) {
return apiClient.get(`/users/${id}`);
}
}
👉 This is exactly POM for API: - API endpoint = locator - Method = action
🧩 user.model.ts (data contract)
export interface User {
id?: string;
name: string;
email: string;
}
4️⃣ Test Layer – Where UI & API MEET¶
📁 Tests
src/tests/
├── ui/
│ └── basicForm.ui.spec.ts
├── api/
│ └── user.api.spec.ts
└── hybrid/
└── user.e2e.spec.ts
🧪 UI Test (UI only)
it('Submit form successfully', async () => {
const home = new HomePage(page);
const form = new BasicFormPage(page);
await home.open('/');
await home.goToBasicForm();
await form.fillUsername('Van QA');
await form.submit();
});
🧪 API Test (API only)
it('Create user by API', async () => {
const userApi = new UserApi();
const res = await userApi.createUser({
name: 'Van QA',
email: 'qa@test.com'
});
expect(res.status).to.equal(201);
});
🧪 Hybrid Test (API setup – UI verify)
it('Create user via API, verify via UI', async () => {
const userApi = new UserApi();
const user = await userApi.createUser({ name: 'Van QA' });
const page = ctx.page;
await page.goto(`/users/${user.data.id}`);
await expect(page.getByText('Van QA')).toBeVisible();
});
5️⃣ Summary (Very Important)¶
🧠 One "Brain-Clincher" Sentence:
UI POM = Page + Locator + Action API POM = Client + Endpoint + Method Test = orchestration (coordination)
6️⃣ Why "Unclear" Structure is Completely Normal?¶
👉 Because:
- 90% tutorials mix test + page logic
- API is usually written inline
- No one clearly explains "API also has POM, just different name"
🇻🇳 PHIÊN BẢN TIẾNG VIỆT¶
1️⃣ Trước hết: POM là gì – và áp dụng cho UI & API ra sao?¶
📌 POM (Page Object Model) – bản chất
POM = tách "logic thao tác" ra khỏi "logic test"
Với UI: - Page = 1 trang web - Page Object chứa: - locator (định vị phần tử) - action (click, fill, submit...)
Với API: - Không có "page" - Tư duy tương đương: - Service / Client Object Model - Nhiều team vẫn gọi là API POM cho dễ hiểu
2️⃣ Cấu trúc CHUẨN – RÕ RÀNG UI POM¶
📁 UI Layer (chỉ UI, không API, không assertion)
src/ui/
├── pages/
│ ├── BasePage.ts
│ ├── HomePage.ts
│ └── BasicFormPage.ts
│
└── components/ (optional – level cao hơn)
└── Header.ts
🧩 BasePage.ts (nền tảng UI)
import { Page } from 'playwright';
export abstract class BasePage {
protected page: Page;
constructor(page: Page) {
this.page = page;
}
async open(path: string) {
await this.page.goto(path);
}
}
🧩 HomePage.ts (POM đúng nghĩa)
import { BasePage } from './BasePage';
export class HomePage extends BasePage {
private basicFormLink = this.page.getByRole('link', {
name: 'Basic HTML Form Example'
});
async goToBasicForm() {
await this.basicFormLink.click();
}
}
🧠 Quy tắc vàng của UI POM
- ❌ Không expect/không assertion
- ❌ Không logic test
- ✅ Chỉ locator + action
🧩 BasicFormPage.ts
export class BasicFormPage extends BasePage {
private usernameInput = this.page.locator('#username');
private submitBtn = this.page.getByRole('button', { name: 'Submit' });
async fillUsername(name: string) {
await this.usernameInput.fill(name);
}
async submit() {
await this.submitBtn.click();
}
}
3️⃣ API "POM" – Tư duy ĐÚNG cho người mới học¶
API không phải Page, nên ta đổi tên cho đúng bản chất.
📁 API Layer (Service / Client)
src/api/
├── clients/
│ └── user.api.ts
│
├── models/
│ └── user.model.ts
│
└── helpers/
└── apiClient.ts
🧩 apiClient.ts (base cho API)
import axios from 'axios';
import { ENV } from '../../utils/env';
export const apiClient = axios.create({
baseURL: ENV.apiUrl,
timeout: 5000,
});
🧩 user.api.ts (API POM)
import { apiClient } from '../helpers/apiClient';
export class UserApi {
async createUser(payload: any) {
return apiClient.post('/users', payload);
}
async getUser(id: string) {
return apiClient.get(`/users/${id}`);
}
}
👉 Đây chính là POM cho API: - API endpoint = locator - Method = action
🧩 user.model.ts (data contract)
export interface User {
id?: string;
name: string;
email: string;
}
4️⃣ Test Layer – nơi UI & API GẶP NHAU¶
📁 Tests
src/tests/
├── ui/
│ └── basicForm.ui.spec.ts
├── api/
│ └── user.api.spec.ts
└── hybrid/
└── user.e2e.spec.ts
🧪 UI Test (UI only)
it('Submit form successfully', async () => {
const home = new HomePage(page);
const form = new BasicFormPage(page);
await home.open('/');
await home.goToBasicForm();
await form.fillUsername('Van QA');
await form.submit();
});
🧪 API Test (API only)
it('Create user by API', async () => {
const userApi = new UserApi();
const res = await userApi.createUser({
name: 'Van QA',
email: 'qa@test.com'
});
expect(res.status).to.equal(201);
});
🧪 Hybrid Test (API setup – UI verify)
it('Create user via API, verify via UI', async () => {
const userApi = new UserApi();
const user = await userApi.createUser({ name: 'Van QA' });
const page = ctx.page;
await page.goto(`/users/${user.data.id}`);
await expect(page.getByText('Van QA')).toBeVisible();
});
5️⃣ Tóm lại cho bạn dễ nhớ (rất quan trọng)¶
🧠 Một câu "chốt não":
UI POM = Page + Locator + Action API POM = Client + Endpoint + Method Test = orchestration (điều phối)
6️⃣ Vì sao thấy "không rõ" là hoàn toàn bình thường?¶
👉 Vì:
- 90% tutorial trộn test + page logic
- API thường bị viết inline
- Không ai nói rõ "API cũng có POM, chỉ khác tên"
🎯 KEY TAKEAWAYS / ĐIỂM CHỐT¶
🔍 Why Structure Gets Confusing When Combined:¶
- Different Naming Conventions: UI calls it "Page", API calls it "Client/Service"
- Mixed Responsibilities: Many tutorials put assertions in page objects
- Inline API Calls: API logic often written directly in tests
- Lack of Clear Separation: No dedicated folders for UI vs API layers
✅ Clear Separation = Clear Understanding¶
UI POM Structure:
src/ui/pages/- Page Objects (locators + actions only)src/ui/components/- Reusable UI componentssrc/tests/ui/- UI-only tests
API POM Structure:
src/api/clients/- API client classes (endpoints + methods)src/api/models/- Data contracts/interfacessrc/tests/api/- API-only tests
Hybrid Tests:
- src/tests/hybrid/ or src/tests/e2e/ - Tests combining UI + API
🚀 Benefits of Clear Structure:¶
- Maintainability: Easy to update UI/API changes
- Reusability: Page/Client objects can be reused across tests
- Readability: Test intent is clear, logic is separated
- Scalability: Easy to add new tests without duplication
This comparison helps explain why POM structure becomes unclear when mixing UI and API testing approaches. 🎯