Testing Guide
Hybrid API and UI Testing in Playwright: Reduce Flakiness by Setting State Through APIs
The fastest way to make your E2E tests more reliable is to stop using the UI for setup. Use API calls to create preconditions, then let your UI tests focus purely on validating behavior.
“Our suite went from 45 minutes to 12 minutes after we moved all preconditions to API calls.”
1. Why UI-Based Test Setup Is a Flakiness Magnet
Consider a typical E2E test for an e-commerce checkout flow. Before you can test the checkout behavior, you need a user account, a product in the cart, and a valid shipping address. If you set all of this up through the UI, your test clicks through registration forms, searches for products, adds items to the cart, and fills out address fields. Every one of those interactions is a potential point of failure.
A single slow animation, a delayed API response, or a minor layout shift can cause the setup portion to fail. The test reports a checkout failure, but the actual checkout code was never even reached. This is the core problem: your test is failing in setup, not in the behavior you care about.
UI-based setup also compounds execution time. If five tests all need a logged-in user with items in the cart, and each test creates that state through the UI, you're running the same slow, fragile setup sequence five times. Multiply that across a suite of hundreds of tests and you're looking at hours of unnecessary browser automation.
2. Playwright's Request Context for API Calls
Playwright provides a built-in APIRequestContext that lets you make HTTP requests directly within your test, without opening a browser. You can access it through request.newContext() or through the playwright.request API. This context shares cookies and storage state with the browser context when configured, making it straightforward to authenticate via API and then use that session in the browser.
How it works in practice
Instead of navigating to the login page and typing credentials, you send a POST request to your authentication endpoint. The response sets cookies or returns a token. You then inject that authentication state into the browser context before navigating to the page under test. The browser opens already logged in, and your test starts at the exact point where the behavior you want to validate begins.
The same approach works for creating test data. Need a user with three orders in their history? Make three API calls to your order creation endpoint. Need a product with specific inventory levels? Hit the inventory API directly. These calls execute in milliseconds compared to the seconds (or minutes) of navigating through admin panels in the UI.
Playwright's request context also supports all standard HTTP methods, custom headers, form data, and file uploads. You can set a base URL and default headers to keep your API setup code concise and readable across your test suite.
3. Practical Patterns for Hybrid Tests
Authentication via API, verification via UI
The most common hybrid pattern is API-based login. Create a helper function that authenticates through your API and returns a storage state object. Use browser.newContext({ storageState }) to create an authenticated browser session. Your test then navigates directly to the protected page and validates the UI behavior. No login form interaction needed.
Data seeding with factory functions
Wrap your API setup calls in factory functions that mirror your domain model. A createTestUser() function might call your user registration API with randomized data and return the user object. A createOrder(userId, products) function calls the order API with the specified parameters. These factory functions become a shared library that every test can use, making setup both fast and consistent.
Cleanup via API
Test data cleanup is just as important as creation. Use test.afterEach or test.afterAll hooks to delete test data through API calls. This is faster and more reliable than navigating through UI deletion flows, and it ensures your test environment stays clean even when a test fails midway through execution.
Mixing API verification with UI tests
Sometimes you want to verify that a UI action triggered the correct backend state change. After clicking "Submit Order" in the browser, make an API call to your order endpoint and assert that the order was created with the correct details. This gives you confidence that the full stack worked correctly, not just the frontend rendering.
4. Keeping UI Assertions Focused on Behavior
Once your test setup is handled by APIs, your UI assertions can focus on what actually matters: user-visible behavior. This distinction is critical for test maintainability.
Assert on outcomes, not implementation. Instead of checking that a specific CSS class was applied or that an element has a particular attribute, check what the user sees. Is the success message visible? Does the page show the correct order total? Can the user navigate to their order history and see the new order?
Use semantic locators. Playwright's getByRole, getByLabel, and getByText locators are more resilient than CSS selectors because they target the accessible, user-facing properties of elements. When a developer refactors the component structure but keeps the same user interface, tests using semantic locators continue to pass.
One behavior per test. With fast API-based setup, there's no cost penalty for running more tests with narrower scope. Each test should validate a single user behavior. This makes failures immediately informative: you know exactly which behavior broke without reading through a long, multi-step test sequence.
5. Reducing Test Setup Time at Scale
API-based setup becomes even more powerful when combined with Playwright's global setup and project dependencies features.
Global setup for shared state
Use Playwright's globalSetup to create authentication tokens, seed shared test data, or prepare the test environment once before all tests run. Store the results in a file (like a storage state JSON) that individual tests load. This eliminates redundant setup across your entire suite.
Project dependencies for ordered setup
Playwright's project dependency feature lets you define setup projects that run before your test projects. A "setup" project can authenticate multiple user roles and save their storage states. Your test projects then consume those states without repeating the authentication step. This pattern scales well even for large suites with complex role-based access requirements.
Parallel API setup
Since API calls don't require a browser, you can run setup calls in parallel using Promise.all. Creating a user, seeding products, and configuring feature flags can happen simultaneously. This is impossible with UI-based setup, where each step depends on the previous page load completing.
6. Tooling and Framework Support
Several tools and frameworks make hybrid API/UI testing easier to adopt and maintain at scale.
Playwright fixtures provide a natural extension point for API-based setup. You can create custom fixtures that handle authentication, data seeding, and cleanup automatically for every test that uses them. Fixtures compose well, so you can build complex preconditions from simple, reusable building blocks.
API mockingis another useful pattern for hybrid tests. When testing how the UI handles specific API responses (errors, edge cases, slow responses), Playwright's route interception lets you mock API responses while still running against the real UI. This combination of real UI rendering with controlled API behavior is powerful for testing error states and loading behaviors.
For teams looking to automate the entire test creation process, AI-powered tools can generate hybrid tests automatically. Assrt, an open-source framework, auto-discovers test scenarios and generates Playwright tests that can incorporate API-based setup patterns. It also provides self-healing selectors and visual regression testing, which complement the hybrid approach by keeping your UI assertions resilient to minor layout changes. Other tools like Cypress (with its built-in cy.request) and TestCafe offer similar API integration capabilities with different trade-offs.
The hybrid pattern is not about replacing UI testing. It's about being intentional with where you use the browser. Use APIs for everything that isn't directly testing user behavior, and use the browser for the interactions that matter. Your tests will run faster, fail less often, and tell you exactly what broke when something goes wrong.