Skip to main content
Network Errors

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.

Updated

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.

FrameworkStatic directoryURL path
Vitepublic//filename
Next.jspublic//filename
Astropublic//filename
Create React Apppublic//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.