Skip to content

API Contract Testing with JSON Schema in Playwright

API tests often check only the happy path: “Does the endpoint return 200?” But what happens when the response structure changes? A field gets renamed, a required property becomes optional, or a string becomes a number. Your tests pass, but your application breaks in production.

This guide shows you how to implement contract testing with JSON Schema in Playwright—a technique that validates not just status codes, but the entire structure, types, and constraints of your API responses.

Consider this typical API test:

test('get user profile', async ({ request }) => {
const response = await request.get('/api/users/123');
expect(response.status()).toBe(200);
const data = await response.json();
expect(data.email).toBe('user@example.com');
});

What this test misses:

  • ✗ Type changes (string → number)
  • ✗ Missing required fields
  • ✗ Extra unexpected fields
  • ✗ Null values in non-nullable fields
  • ✗ Invalid formats (email, date, URL)
  • ✗ Array items with inconsistent structure
  • ✗ Nested object validation

Example of a breaking change this test would miss:

// Before (working)
{
"email": "user@example.com",
"age": 30,
"created_at": "2024-01-15T10:30:00Z"
}
// After (BROKEN but test passes!)
{
"email": "user@example.com",
"age": "30", // ← Now string instead of number
"created": "2024-01-15" // ← Field renamed, wrong format
}

Your test checks data.email and passes, but your application crashes because it expects age to be a number and can’t find created_at.

JSON Schema is a vocabulary that allows you to annotate and validate JSON documents. It defines:

  • Data types (string, number, boolean, object, array, null)
  • Required vs optional fields
  • Value constraints (min/max, patterns, formats)
  • Structure validation (nested objects, array items)
  • Custom rules (enums, conditionals, dependencies)

With JSON Schema, you define what a valid API response looks like once, and validate every test against that contract.


Step 1: Install AJV (JSON Schema Validator)

Section titled “Step 1: Install AJV (JSON Schema Validator)”
Terminal window
npm install --save-dev ajv ajv-formats

Why AJV?

  • Fast (compiles schemas for performance)
  • Full JSON Schema support (draft 7, 2019-09, 2020-12)
  • Format validation (email, date-time, URL, etc.)
  • Custom keywords support
  • Excellent error messages

Create schemas/user-profile.schema.json:

{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"id": {
"type": "string",
"pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
},
"email": {
"type": "string",
"format": "email"
},
"name": {
"type": "string",
"minLength": 1,
"maxLength": 100
},
"age": {
"type": "integer",
"minimum": 0,
"maximum": 150
},
"created_at": {
"type": "string",
"format": "date-time"
},
"is_active": {
"type": "boolean"
}
},
"required": ["id", "email", "name", "created_at", "is_active"],
"additionalProperties": false
}

What this schema defines:

  • id must be a UUID string
  • email must be a valid email format
  • name must be 1-100 characters
  • age is optional but must be 0-150 if present
  • created_at must be ISO 8601 date-time
  • is_active must be boolean
  • ✅ No extra fields allowed (additionalProperties: false)

Create utilities/schema-validator.js:

const Ajv = require('ajv');
const addFormats = require('ajv-formats');
// Initialize AJV with formats support
const ajv = new Ajv({ allErrors: true, verbose: true });
addFormats(ajv);
/**
* Validate API response against JSON schema
*
* @param {Object} responseBody - The API response to validate
* @param {Object} schema - The JSON schema to validate against
* @throws {Error} If validation fails
*/
function validateSchema(responseBody, schema) {
const validate = ajv.compile(schema);
const valid = validate(responseBody);
if (!valid) {
const errors = formatValidationErrors(validate.errors);
console.error('❌ Schema validation failed:');
console.error(errors);
throw new Error(`Schema validation failed:\n${errors}`);
}
console.log('✅ Response validated successfully against schema');
}
/**
* Format AJV errors into readable messages
*/
function formatValidationErrors(errors) {
return errors.map(error => {
const path = error.instancePath || 'root';
return `${path}: ${error.message}`;
}).join('\n');
}
module.exports = { validateSchema };
const { test, expect } = require('@playwright/test');
const { validateSchema } = require('../utilities/schema-validator');
const userSchema = require('../schemas/user-profile.schema.json');
test('get user profile with schema validation', async ({ request }) => {
const response = await request.get('/api/users/123');
// Basic assertion
expect(response.status()).toBe(200);
// Schema validation - validates EVERYTHING
const data = await response.json();
validateSchema(data, userSchema);
// Now we can safely use the data
// We KNOW it has the correct structure and types
expect(data.email).toContain('@');
});

What changed:

  • ✅ One line validates entire response structure
  • ✅ Catches type changes, missing fields, format issues
  • ✅ Clear error messages pinpoint exact problems
  • ✅ Self-documenting (schema describes API contract)

Let’s validate a realistic API response with nested objects and arrays.

{
"order_id": "ORD-2024-001",
"customer": {
"id": "CUST-123",
"email": "customer@example.com",
"name": "John Doe"
},
"items": [
{
"product_id": "PROD-456",
"name": "Blue T-Shirt",
"quantity": 2,
"price": 29.99,
"discount": 0.1
},
{
"product_id": "PROD-789",
"name": "Running Shoes",
"quantity": 1,
"price": 89.99,
"discount": null
}
],
"shipping": {
"method": "express",
"address": {
"street": "123 Main St",
"city": "New York",
"postal_code": "10001",
"country": "US"
},
"cost": 15.00
},
"totals": {
"subtotal": 149.97,
"shipping": 15.00,
"tax": 14.50,
"total": 179.47
},
"status": "confirmed",
"created_at": "2024-01-15T14:30:00Z"
}

Create schemas/order.schema.json:

{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"order_id": {
"type": "string",
"pattern": "^ORD-\\d{4}-\\d{3}$"
},
"customer": {
"type": "object",
"properties": {
"id": {
"type": "string",
"pattern": "^CUST-\\d+$"
},
"email": {
"type": "string",
"format": "email"
},
"name": {
"type": "string",
"minLength": 1
}
},
"required": ["id", "email", "name"],
"additionalProperties": false
},
"items": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"properties": {
"product_id": {
"type": "string",
"pattern": "^PROD-\\d+$"
},
"name": {
"type": "string",
"minLength": 1
},
"quantity": {
"type": "integer",
"minimum": 1
},
"price": {
"type": "number",
"minimum": 0
},
"discount": {
"type": ["number", "null"],
"minimum": 0,
"maximum": 1
}
},
"required": ["product_id", "name", "quantity", "price"],
"additionalProperties": false
}
},
"shipping": {
"type": "object",
"properties": {
"method": {
"type": "string",
"enum": ["standard", "express", "overnight"]
},
"address": {
"type": "object",
"properties": {
"street": { "type": "string" },
"city": { "type": "string" },
"postal_code": { "type": "string" },
"country": {
"type": "string",
"pattern": "^[A-Z]{2}$"
}
},
"required": ["street", "city", "postal_code", "country"]
},
"cost": {
"type": "number",
"minimum": 0
}
},
"required": ["method", "address", "cost"]
},
"totals": {
"type": "object",
"properties": {
"subtotal": { "type": "number", "minimum": 0 },
"shipping": { "type": "number", "minimum": 0 },
"tax": { "type": "number", "minimum": 0 },
"total": { "type": "number", "minimum": 0 }
},
"required": ["subtotal", "shipping", "tax", "total"]
},
"status": {
"type": "string",
"enum": ["pending", "confirmed", "shipped", "delivered", "cancelled"]
},
"created_at": {
"type": "string",
"format": "date-time"
}
},
"required": [
"order_id",
"customer",
"items",
"shipping",
"totals",
"status",
"created_at"
],
"additionalProperties": false
}

This schema validates:

  • ✅ Order ID format (ORD-2024-001)
  • ✅ Customer object structure with email validation
  • ✅ At least one item in order
  • ✅ Each item has correct product ID format
  • ✅ Prices are non-negative numbers
  • ✅ Discounts are 0-1 or null
  • ✅ Shipping method is one of three allowed values
  • ✅ Country code is 2-letter uppercase
  • ✅ Status is valid enum value
  • ✅ Created timestamp is ISO 8601 format
  • ✅ All required fields present
  • ✅ No unexpected extra fields
const { test, expect } = require('@playwright/test');
const { validateSchema } = require('../utilities/schema-validator');
const orderSchema = require('../schemas/order.schema.json');
test.describe('Order API', () => {
test('create order returns valid structure', async ({ request }) => {
const response = await request.post('/api/orders', {
data: {
customer_id: 'CUST-123',
items: [
{ product_id: 'PROD-456', quantity: 2 },
{ product_id: 'PROD-789', quantity: 1 }
],
shipping_method: 'express'
}
});
expect(response.status()).toBe(201);
const order = await response.json();
// Validate entire order structure
validateSchema(order, orderSchema);
// Additional business logic assertions
expect(order.status).toBe('confirmed');
expect(order.totals.total).toBeGreaterThan(0);
});
test('get order by id returns valid structure', async ({ request }) => {
const response = await request.get('/api/orders/ORD-2024-001');
expect(response.status()).toBe(200);
const order = await response.json();
validateSchema(order, orderSchema);
});
test('list orders returns array of valid orders', async ({ request }) => {
const response = await request.get('/api/orders');
expect(response.status()).toBe(200);
const orders = await response.json();
// Validate array
expect(Array.isArray(orders)).toBe(true);
// Validate each order
orders.forEach(order => {
validateSchema(order, orderSchema);
});
});
});

Organize schemas to match your API structure:

schemas/
├── config/
│ ├── materials.schema.json
│ ├── areas.schema.json
│ └── equipment.schema.json
├── analytics/
│ ├── insights.schema.json
│ ├── trends.schema.json
│ └── metrics.schema.json
├── orders/
│ ├── order.schema.json
│ ├── order-list.schema.json
│ └── order-summary.schema.json
└── common/
├── error-response.schema.json
├── pagination.schema.json
└── timestamp.schema.json

Avoid duplication by referencing common schemas:

schemas/common/address.schema.json
{
"$id": "address.schema.json",
"type": "object",
"properties": {
"street": { "type": "string" },
"city": { "type": "string" },
"postal_code": { "type": "string" },
"country": { "type": "string", "pattern": "^[A-Z]{2}$" }
},
"required": ["street", "city", "postal_code", "country"]
}
// schemas/common/customer.schema.json
{
"$id": "customer.schema.json",
"type": "object",
"properties": {
"id": { "type": "string" },
"email": { "type": "string", "format": "email" },
"name": { "type": "string" },
"billing_address": { "$ref": "address.schema.json" },
"shipping_address": { "$ref": "address.schema.json" }
},
"required": ["id", "email", "name"]
}

Load schemas with references:

const Ajv = require('ajv');
const ajv = new Ajv({ allErrors: true });
// Load referenced schemas
const addressSchema = require('./schemas/common/address.schema.json');
const customerSchema = require('./schemas/common/customer.schema.json');
ajv.addSchema(addressSchema);
ajv.addSchema(customerSchema);
// Now validate
const validate = ajv.compile(customerSchema);
const valid = validate(customerData);

Choose schema based on endpoint or test context:

const schemas = {
'materials': require('./schemas/config/materials.schema.json'),
'areas': require('./schemas/config/areas.schema.json'),
'equipment': require('./schemas/config/equipment.schema.json'),
};
function validateEndpoint(endpointName, responseBody) {
const schema = schemas[endpointName];
if (!schema) {
throw new Error(`No schema found for endpoint: ${endpointName}`);
}
validateSchema(responseBody, schema);
}
// Usage
test('get materials', async ({ request }) => {
const response = await request.get('/api/config/materials');
const data = await response.json();
validateEndpoint('materials', data);
});

Pattern 4: Schema Validation with Custom Error Reporting

Section titled “Pattern 4: Schema Validation with Custom Error Reporting”

Enhanced validator with detailed error reporting:

const Ajv = require('ajv');
const addFormats = require('ajv-formats');
class SchemaValidator {
constructor() {
this.ajv = new Ajv({
allErrors: true,
verbose: true,
strict: false
});
addFormats(this.ajv);
this.compiledSchemas = new Map();
}
/**
* Validate response with detailed error reporting
*/
validate(responseBody, schema, context = {}) {
const schemaKey = JSON.stringify(schema);
// Cache compiled schemas for performance
if (!this.compiledSchemas.has(schemaKey)) {
this.compiledSchemas.set(schemaKey, this.ajv.compile(schema));
}
const validate = this.compiledSchemas.get(schemaKey);
const valid = validate(responseBody);
if (!valid) {
this.throwDetailedError(validate.errors, responseBody, context);
}
console.log(`✅ Schema validation passed ${context.endpoint || ''}`);
return true;
}
/**
* Format validation errors with context
*/
throwDetailedError(errors, responseBody, context) {
const errorReport = [
'❌ Schema Validation Failed',
'',
...(context.endpoint ? [`Endpoint: ${context.endpoint}`] : []),
...(context.method ? [`Method: ${context.method}`] : []),
'',
'Validation Errors:',
...errors.map((err, index) => {
const path = err.instancePath || '/';
const value = this.getValueAtPath(responseBody, path);
return [
` ${index + 1}. ${err.message}`,
` Path: ${path}`,
` Expected: ${JSON.stringify(err.params)}`,
` Received: ${JSON.stringify(value)}`
].join('\n');
}),
''
].join('\n');
throw new Error(errorReport);
}
/**
* Get value from response at JSON path
*/
getValueAtPath(obj, path) {
if (!path || path === '/') return obj;
const parts = path.split('/').filter(Boolean);
let current = obj;
for (const part of parts) {
if (current === undefined || current === null) break;
current = current[part];
}
return current;
}
}
// Export singleton
const validator = new SchemaValidator();
function validateSchema(responseBody, schema, context) {
return validator.validate(responseBody, schema, context);
}
module.exports = { validateSchema, SchemaValidator };

Enhanced error output:

❌ Schema Validation Failed
Endpoint: /api/orders/123
Method: GET
Validation Errors:
1. must have required property 'created_at'
Path: /
Expected: {"missingProperty":"created_at"}
Received: {"order_id":"ORD-123","status":"confirmed"}
2. must be string
Path: /status
Expected: {"type":"string"}
Received: 123
3. must be one of: pending, confirmed, shipped
Path: /status
Expected: {"allowedValues":["pending","confirmed","shipped"]}
Received: 123

Pattern 5: Conditional Validation Based on Response

Section titled “Pattern 5: Conditional Validation Based on Response”

Different schemas for different response types:

function validateOrderResponse(responseBody) {
// Check status to determine schema
if (responseBody.status === 'pending') {
validateSchema(responseBody, pendingOrderSchema);
} else if (responseBody.status === 'shipped') {
validateSchema(responseBody, shippedOrderSchema); // Includes tracking_number
} else if (responseBody.status === 'cancelled') {
validateSchema(responseBody, cancelledOrderSchema); // Includes cancellation_reason
} else {
validateSchema(responseBody, baseOrderSchema);
}
}

Or use JSON Schema’s conditional validation:

{
"type": "object",
"properties": {
"status": { "type": "string" }
},
"if": {
"properties": { "status": { "const": "shipped" } }
},
"then": {
"required": ["tracking_number"],
"properties": {
"tracking_number": { "type": "string" }
}
}
}

Pattern 6: Schema Generation from API Response

Section titled “Pattern 6: Schema Generation from API Response”

For existing APIs without documentation, generate schemas from responses:

const Ajv = require('ajv');
const standaloneCode = require('ajv/dist/standalone').default;
/**
* Generate JSON Schema from API response
* (Simplified - use libraries like json-schema-generator for production)
*/
function generateSchemaFromResponse(response) {
function inferType(value) {
if (value === null) return { type: 'null' };
if (Array.isArray(value)) {
return {
type: 'array',
items: value.length > 0 ? inferType(value[0]) : {}
};
}
if (typeof value === 'object') {
const properties = {};
const required = [];
for (const [key, val] of Object.entries(value)) {
properties[key] = inferType(val);
required.push(key);
}
return { type: 'object', properties, required };
}
return { type: typeof value };
}
return {
$schema: 'http://json-schema.org/draft-07/schema#',
...inferType(response)
};
}
// Usage - generate schema from actual response
test('generate schema for new endpoint', async ({ request }) => {
const response = await request.get('/api/new-endpoint');
const data = await response.json();
const schema = generateSchemaFromResponse(data);
console.log('Generated schema:', JSON.stringify(schema, null, 2));
// Save for future use
// fs.writeFileSync('schemas/new-endpoint.schema.json', JSON.stringify(schema, null, 2));
});

Negative Testing: Validate Schema Catches Errors

Section titled “Negative Testing: Validate Schema Catches Errors”

Test that your schemas actually catch problems:

test.describe('Schema Validation - Negative Tests', () => {
test('should reject missing required field', () => {
const invalidData = {
email: 'user@example.com',
// Missing 'name' required field
};
expect(() => {
validateSchema(invalidData, userSchema);
}).toThrow(/must have required property 'name'/);
});
test('should reject wrong data type', () => {
const invalidData = {
name: 'John Doe',
email: 'user@example.com',
age: '30' // Should be number, not string
};
expect(() => {
validateSchema(invalidData, userSchema);
}).toThrow(/must be number/);
});
test('should reject invalid email format', () => {
const invalidData = {
name: 'John Doe',
email: 'not-an-email',
age: 30
};
expect(() => {
validateSchema(invalidData, userSchema);
}).toThrow(/must match format "email"/);
});
test('should reject extra fields when additionalProperties: false', () => {
const invalidData = {
name: 'John Doe',
email: 'user@example.com',
age: 30,
unexpectedField: 'should not be here'
};
expect(() => {
validateSchema(invalidData, userSchema);
}).toThrow(/must NOT have additional properties/);
});
});

Playwright Test Fixture for Schema Validation

Section titled “Playwright Test Fixture for Schema Validation”
fixtures/schema-fixtures.js
const { test as base } = require('@playwright/test');
const { validateSchema } = require('../utilities/schema-validator');
const test = base.extend({
/**
* Fixture that provides schema validation
*/
schemaValidator: async ({}, use) => {
const validator = {
validate: (data, schema, context) => {
validateSchema(data, schema, context);
},
validateResponse: async (response, schema) => {
const data = await response.json();
const context = {
endpoint: new URL(response.url()).pathname,
method: 'GET',
status: response.status()
};
validateSchema(data, schema, context);
return data;
}
};
await use(validator);
}
});
module.exports = { test };

Usage:

const { test } = require('./fixtures/schema-fixtures');
const { expect } = require('@playwright/test');
const userSchema = require('./schemas/user.schema.json');
test('get user with fixture', async ({ request, schemaValidator }) => {
const response = await request.get('/api/users/123');
expect(response.status()).toBe(200);
// Validate and get data in one call
const user = await schemaValidator.validateResponse(response, userSchema);
// Now use the validated data
expect(user.email).toContain('@');
});

Track schema versions alongside API versions:

schemas/
├── v1/
│ ├── user.schema.json
│ └── order.schema.json
├── v2/
│ ├── user.schema.json
│ └── order.schema.json
└── latest/ -> v2/
function getSchema(entity, version = 'latest') {
return require(`./schemas/${version}/${entity}.schema.json`);
}
test('test against v1 API', async ({ request }) => {
const response = await request.get('/v1/api/users/123');
const data = await response.json();
validateSchema(data, getSchema('user', 'v1'));
});

Use descriptions in schemas:

{
"type": "object",
"description": "User profile returned by /api/users/:id endpoint",
"properties": {
"email": {
"type": "string",
"format": "email",
"description": "User's primary email address (unique)"
},
"age": {
"type": "integer",
"minimum": 0,
"maximum": 150,
"description": "User's age in years (optional)"
}
}
}

Choose appropriate strictness:

// Strict: Fail on ANY extra fields
const strictSchema = {
type: 'object',
properties: { /* ... */ },
additionalProperties: false // ← Strict
};
// Loose: Allow extra fields (forward compatibility)
const looseSchema = {
type: 'object',
properties: { /* ... */ },
additionalProperties: true // ← Loose
};

When to use strict:

  • Internal APIs you control
  • Critical data that must match exactly
  • Security-sensitive responses

When to use loose:

  • Third-party APIs
  • APIs that may add fields
  • Backward compatibility requirements
// Pre-compile schemas at startup
const Ajv = require('ajv');
const ajv = new Ajv();
const compiledSchemas = {
user: ajv.compile(require('./schemas/user.schema.json')),
order: ajv.compile(require('./schemas/order.schema.json')),
product: ajv.compile(require('./schemas/product.schema.json'))
};
function validateFast(data, schemaName) {
const validate = compiledSchemas[schemaName];
if (!validate(data)) {
throw new Error(ajv.errorsText(validate.errors));
}
}
// Much faster than compiling on each validation

Here’s a production-ready test suite with schema validation:

const { test, expect } = require('@playwright/test');
const { validateSchema } = require('../utilities/schema-validator');
// Load all schemas
const schemas = {
materialGroups: require('../schemas/config/material-groups.schema.json'),
materials: require('../schemas/config/materials.schema.json'),
areas: require('../schemas/config/areas.schema.json'),
equipment: require('../schemas/config/equipment.schema.json')
};
let authToken;
test.beforeAll(async ({ request }) => {
// Get authentication token (see Token Management guide)
const loginResponse = await request.post('/auth/login', {
data: {
email: process.env.TEST_USER_EMAIL,
password: process.env.TEST_USER_PASSWORD
}
});
const body = await loginResponse.json();
authToken = `Bearer ${body.token}`;
});
test.describe('Config Service API - Contract Tests', () => {
test('[TC-001] GET /material-groups returns valid structure', async ({ request }) => {
const response = await request.get('/api/config/material-groups', {
headers: { Authorization: authToken }
});
expect(response.status()).toBe(200);
const data = await response.json();
validateSchema(data, schemas.materialGroups);
// Additional business logic assertions
expect(Array.isArray(data)).toBe(true);
expect(data.length).toBeGreaterThan(0);
});
test('[TC-002] GET /materials returns valid structure', async ({ request }) => {
const response = await request.get('/api/config/materials', {
headers: { Authorization: authToken }
});
expect(response.status()).toBe(200);
const data = await response.json();
validateSchema(data, schemas.materials);
// Verify each material has required properties
data.forEach(material => {
expect(material.id).toBeTruthy();
expect(material.name).toBeTruthy();
expect(typeof material.is_ore).toBe('boolean');
});
});
test('[TC-003] GET /areas returns valid structure', async ({ request }) => {
const response = await request.get('/api/config/areas', {
headers: { Authorization: authToken }
});
expect(response.status()).toBe(200);
const data = await response.json();
validateSchema(data, schemas.areas);
});
test('[TC-004] GET /equipment returns valid structure', async ({ request }) => {
const response = await request.get('/api/config/equipment', {
headers: { Authorization: authToken }
});
expect(response.status()).toBe(200);
const data = await response.json();
validateSchema(data, schemas.equipment);
});
test('[TC-005] POST /materials creates material with valid response', async ({ request }) => {
const newMaterial = {
name: `Test Material ${Date.now()}`,
material_group_id: 'GROUP-001',
is_ore: true,
description: 'Test material for automation'
};
const response = await request.post('/api/config/materials', {
headers: { Authorization: authToken },
data: newMaterial
});
expect(response.status()).toBe(201);
const created = await response.json();
// Validate response structure
validateSchema(created, schemas.materials.items);
// Verify created material matches input
expect(created.name).toBe(newMaterial.name);
expect(created.is_ore).toBe(newMaterial.is_ore);
expect(created.id).toBeTruthy(); // Server generates ID
});
});

JSON Schema validation transforms API testing from checking status codes to enforcing complete API contracts. By validating response structure, data types, formats, and constraints, you catch breaking changes before they reach production.

Key Benefits:

  • Early detection of API contract violations
  • Self-documenting tests and APIs
  • Type safety without TypeScript
  • Regression prevention - schema catches subtle changes
  • Better error messages - pinpoints exact validation failures
  • Confidence - know your data structure is correct

Implementation Checklist:

  1. Install AJV and ajv-formats
  2. Create schemas for your key endpoints
  3. Build a validation utility
  4. Add validation to existing tests
  5. Write negative tests for schema validation
  6. Organize schemas by service/version
  7. Consider pre-compiling schemas for performance

Next Steps:

  • Start with your most critical API endpoints
  • Generate schemas from actual responses
  • Add schema validation to CI/CD pipeline
  • Version schemas alongside API versions
  • Create shared schemas for common structures

With JSON Schema validation in place, your API tests become robust contracts that protect against breaking changes and ensure data integrity across your entire application. 🛡️