SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON
SyntaxError: Unexpected token < in JSON at position 0
You called response.json() but the server returned HTML (probably an error page) instead of JSON. The < is the start of <!DOCTYPE html>.
Fix 1: Wrong URL
The most common cause. Your fetch URL is wrong, and the server returned a 404 HTML page.
// β Typo in URL β server returns 404 HTML page
const res = await fetch('/api/uers'); // "uers" not "users"
// β
Correct URL
const res = await fetch('/api/users');
Always check the response before parsing:
const res = await fetch('/api/users');
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
}
const data = await res.json();
Fix 2: API Server Not Running
Your frontend is running but the API server isnβt. The dev server returns its own HTML 404 page.
# Check if the API is actually running
curl http://localhost:3000/api/users
If you get HTML back, your API isnβt running or isnβt on that port.
Fix 3: Proxy Not Configured (Vite / CRA)
In development, your frontend runs on port 5173 and your API on port 3000. Without a proxy, /api/users hits the frontend server.
// vite.config.ts
export default {
server: {
proxy: {
'/api': 'http://localhost:3000',
},
},
};
Fix 4: CORS Redirect
The server redirected your API request to a login page (HTML).
// Check what you actually got back
const res = await fetch('/api/users');
const text = await res.text();
console.log(text); // See the actual response
If itβs a login page, you need to handle authentication first.
Fix 5: Production Build Serving HTML
In production, a catch-all route (for SPA routing) might serve index.html for API routes too.
# β Nginx catches /api routes and serves index.html
location / {
try_files $uri $uri/ /index.html;
}
# β
API routes go to the backend first
location /api/ {
proxy_pass http://localhost:3000;
}
location / {
try_files $uri $uri/ /index.html;
}
Debugging Pattern
Always check what you got before parsing:
const res = await fetch(url);
const contentType = res.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
const text = await res.text();
console.error('Expected JSON, got:', text.substring(0, 200));
throw new Error('Response is not JSON');
}
const data = await res.json();