How to Fix 404 Not Found Errors During Development
Common causes of 404 errors in local development and how to fix broken routes, missing assets, and misconfigured dev servers.
How to Fix 404 Not Found Errors During Development
Your dev server is running. The page loads. But your image is broken, your API call returns a 404, or navigating to a route shows a blank page. The console shows:
GET http://localhost:3000/images/logo.png 404 (Not Found)
A 404 in production means the resource doesn’t exist at that URL. A 404 in development usually means one of six things: the path is wrong, the case doesn’t match, the dev server isn’t configured to serve it, your SPA router doesn’t have a fallback, the file isn’t in the right directory, or your API proxy isn’t set up.
Common causes
1. Wrong file path or typo
The most common cause. You wrote /images/logo.png but the file is at /img/logo.png. Or you wrote ../components/Header but the file is ../components/header.
<!-- Your code -->
<img src="/images/logo.png" />
<!-- Actual file location -->
<!-- public/img/logo.png -->
Fix: Check the exact file path. Open your project’s public/ (or static/, depending on your framework) directory and verify the file exists at the path you referenced.
In most frameworks, files in the public/ directory are served from the root. A file at public/img/logo.png is available at /img/logo.png, not /public/img/logo.png.
<!-- Correct for file at public/img/logo.png -->
<img src="/img/logo.png" />
2. Case sensitivity
macOS and Windows file systems are case-insensitive by default. Logo.png and logo.png resolve to the same file. Linux is case-sensitive. They don’t.
This means your code works on your Mac, then breaks in CI or production (which usually runs Linux).
// Works on macOS, fails on Linux
import Header from './components/header'
// Actual file: ./components/Header.tsx
Fix: Match the case exactly. Always. Configure ESLint to catch this:
{
"plugins": ["import"],
"rules": {
"import/no-unresolved": "error"
},
"settings": {
"import/resolver": {
"typescript": true
}
}
}
If you want to catch this locally on macOS, create a case-sensitive APFS volume for your projects, or use Docker for development.
3. SPA routing without a fallback
Single-page apps handle routing client-side. When you navigate to /dashboard by clicking a link, the React/Vue/Svelte router handles it. But when you type http://localhost:3000/dashboard directly in the address bar or refresh the page, the dev server receives the request first. If it doesn’t know to serve index.html for all routes, it returns a 404.
GET /dashboard 404 (Not Found)
Fix for Vite:
// vite.config.ts
export default defineConfig({
server: {
// Vite does this by default for SPA mode.
// If you're seeing 404s on refresh, check that
// appType is not set to 'mpa'.
},
})
Fix for webpack-dev-server:
// webpack.config.js
module.exports = {
devServer: {
historyApiFallback: true,
},
}
Fix for Express (custom dev server):
// Serve your SPA for any route that doesn't match a static file
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'dist', 'index.html'))
})
This rule must come after your static file middleware and API routes.
4. Assets not in the public directory
Frameworks like Vite, Next.js, and Astro serve static assets from a specific directory. If your file isn’t in that directory, the dev server can’t find it.
| Framework | Static directory | URL path |
|---|---|---|
| Vite | public/ | /filename |
| Next.js | public/ | /filename |
| Astro | public/ | /filename |
| Create React App | public/ | /filename |
Files inside src/ are handled by the bundler and must be imported in your code. They can’t be referenced by URL path.
// File at src/assets/logo.png -- must be imported
import logo from '../assets/logo.png'
// File at public/logo.png -- reference by URL
const logo = '/logo.png'
Fix: Know which directory your framework uses for static files. If the file needs a URL path, put it in public/. If it should be processed by the bundler (optimized, hashed, tree-shaken), keep it in src/ and import it.
5. API route prefix mismatch
Your frontend calls /api/users. Your backend serves routes at /users. Or your proxy is configured for /api but your backend expects /api/v1.
// Frontend
const res = await fetch('/api/users')
// Backend (Express)
app.get('/users', handler) // No /api prefix
Fix: Align the paths, or configure your dev proxy to rewrite them.
// vite.config.ts
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
})
This strips the /api prefix before forwarding to the backend. A request to /api/users becomes /users on the backend.
6. Dev server not watching the right directory
Some setups serve files from dist/ or build/, not src/. If your build hasn’t run, or if the watcher isn’t picking up changes, the dev server serves stale files or returns 404s for new ones.
GET /new-page 404 (Not Found)
Fix: Check your dev server’s root directory setting.
// vite.config.ts
export default defineConfig({
root: '.', // default, serves from project root
publicDir: 'public', // default
})
If you’re using a custom server, make sure it points to the right directory:
app.use(express.static(path.join(__dirname, 'dist')))
After changing config, restart the dev server. Most config changes aren’t picked up by hot reload.
Debugging 404s quickly
Open Chrome DevTools, go to the Network tab, and filter by status code. Click the failed request. The Headers tab shows you the exact URL the browser requested. Compare that to what your server actually serves.
For API routes, test the URL directly with curl:
curl -v http://localhost:8080/users
If curl gets a response but your browser gets a 404, the issue is in your proxy configuration, not your backend.
Prevention
Use path aliases in your bundler config so imports are explicit and consistent. Run your CI on Linux (or a Linux container) to catch case-sensitivity issues before production. Set up your dev proxy once and document it in the project README so every team member has the same setup.
Hushbug catches 404 errors and network failures automatically while you browse. Coming soon to the Chrome Web Store.