Integration is where most capstone projects stall. Each layer works in isolation โ the Express API passes Postman tests, the React components render correctly in Storybook โ but when they connect to each other, things break in ways that did not appear during isolated development. CORS errors, environment variable mismatches, wrong API response shapes, missing auth headers โ these are the bugs that integration tests exist to catch early. In this lesson you will systematically connect the React frontend to the Express API, run the full test suites, write E2E tests for the critical flows, and work through the most common integration problems with a debugging methodology that applies to any MERN project.
Integration Debugging Methodology
When a feature does not work end-to-end, isolate the layer:
Step 1: Is the API working? (Postman)
โ Call the endpoint directly in Postman
โ If it fails: Express bug โ fix in the server layer
โ If it succeeds: the problem is in React or the connection
Step 2: Is the React request reaching the API? (Browser DevTools)
โ Open Network tab โ trigger the action โ find the XHR/Fetch request
โ If no request: Axios call not made โ React bug
โ If request shows "CORS error": fix CORS on Express or proxy
โ If request reaches server but wrong data: check request body and headers
Step 3: Is the response correct? (Browser DevTools โ Response tab)
โ 401? Check Authorization header โ is the token being sent?
โ 404? Check the URL โ does it match the Express route?
โ 422/400? Check the request body โ required fields missing?
โ 200 but wrong data? Check that populate() is called on the right fields
Step 4: Is React using the response correctly?
โ console.log(res.data) โ does the shape match what the component expects?
โ Is it res.data.data or res.data? Check the Express response envelope
/api/... and Vite forwards them to Express at localhost:5000. This avoids CORS in development entirely โ from the browser’s perspective, the React app and the API are on the same origin. In production, Axios uses VITE_API_URL which points to Render. This two-environment setup is the standard MERN development configuration.Vite Proxy Configuration
// client/vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
resolve: {
alias: { '@': path.resolve(__dirname, './src') },
},
server: {
proxy: {
'/api': {
target: 'http://localhost:5000',
changeOrigin: true,
secure: false,
// All /api/* requests are forwarded to Express at localhost:5000
// The browser sees them as same-origin โ no CORS issues in dev
},
},
},
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./src/test/setup.js'],
},
});
Common Integration Problems and Fixes
| Symptom | Likely Cause | Fix |
|---|---|---|
| CORS error in browser | Client URL not in Express CORS allowlist | Add CLIENT_URL to Express CORS, redeploy |
| 401 on every API call | Token not in Authorization header | Verify Axios interceptor reads from localStorage |
| ProtectedRoute redirects when logged in | AuthContext.loading never false | Verify /api/auth/me resolves; check MONGODB_URI |
res.data.data is undefined |
Express returns res.json({ data: post }) not { success, data } |
Standardise all Express responses to { success, data } |
| Socket.io not connecting | Socket.io CORS not configured | Set cors: { origin: CLIENT_URL } on new Server() |
| File upload fails | Content-Type manually set on Axios | Remove manual Content-Type โ Axios sets boundary automatically |
| Post body renders as plain text | dangerouslySetInnerHTML not used |
Use dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(body) }} |
Running the Full Test Suite
# Server integration tests
cd server
npm test -- --forceExit
# Expected: all auth, post, comment, user routes pass
# Client component tests
cd ../client
npm test -- --run
# Expected: PostCard, FormField, LoginPage tests pass
# E2E tests (with both servers running)
cd ..
npx playwright test --headed # see the browser
# Expected: register, login, create post flows pass
Playwright E2E โ Critical Flows
// e2e/full-flow.spec.js โ the complete new-user-to-published-post flow
import { test, expect } from '@playwright/test';
test('new user registers, creates a post, and views it', async ({ page }) => {
const email = `capstone-${Date.now()}@example.com`;
const password = 'TestPass@1234';
// โโ Register โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
await page.goto('/register');
await page.getByLabel('Name').fill('Capstone Author');
await page.getByLabel('Email').fill(email);
await page.getByLabel('Password').fill(password);
await page.getByLabel('Confirm Password').fill(password);
await page.getByRole('button', { name: /create account/i }).click();
await expect(page).toHaveURL(/\/dashboard/);
// โโ Create Post โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
await page.goto('/posts/new');
await page.getByLabel('Title').fill('My Capstone Post');
await page.getByLabel('Content').fill(
'This is the body of my capstone test post with enough words to pass validation.'
);
await page.getByLabel('Publish immediately').check();
await page.getByRole('button', { name: /publish/i }).click();
// Should redirect to the new post
await expect(page).toHaveURL(/\/posts\//);
await expect(page.getByRole('heading', { name: 'My Capstone Post' })).toBeVisible();
// โโ View on Home Page โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
await page.goto('/');
await expect(page.getByText('My Capstone Post')).toBeVisible();
// โโ Logout โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
await page.getByRole('button', { name: /log out/i }).click();
await page.goto('/dashboard');
await expect(page).toHaveURL(/\/login/); // redirected โ no longer authenticated
});
Common Mistakes
Mistake 1 โ Testing without the servers running
โ Wrong โ Playwright tests fail with connection refused:
Error: connect ECONNREFUSED 127.0.0.1:5173
# React dev server is not running
โ
Correct โ use Playwright’s webServer config to auto-start servers, or manually start both before running tests.
Mistake 2 โ Not resetting test data between E2E tests
โ Wrong โ second test fails because the email from the first test is already registered:
await page.getByLabel('Email').fill('test@example.com'); // used in 3 tests
// Second test: 409 Conflict โ email already registered
โ
Correct โ use unique emails with timestamps: `test-${Date.now()}@example.com`.
Mistake 3 โ Skipping the Network tab and guessing at bugs
โ Wrong โ adding console.logs to every React component without first checking DevTools.
โ Correct โ open Network tab, reproduce the bug, read the failing request and response. The cause is visible in 30 seconds 90% of the time.
Quick Reference โ Debugging Checklist
| Check | Where to Look |
|---|---|
| Request URL correct | DevTools Network โ Request URL |
| Auth header present | DevTools Network โ Headers โ Authorization |
| Response shape correct | DevTools Network โ Response tab |
| Server error details | Render logs (or terminal in dev) |
| React state value | React DevTools โ Components tab |
| Socket connection status | Console: socket.connected |