πŸ”§ Error Fixes
Β· 5 min read
Last updated on

Hono: 404 Not Found for All Routes β€” How to Fix It


404 Not Found

Every route in your Hono app returns 404. The server is running, but no routes match. This is almost always a configuration or ordering issue β€” Hono isn’t seeing your route registrations, or the routes don’t match what you’re requesting.

How Hono routing works

Hono matches routes in registration order. When a request comes in, it checks each registered route top to bottom. If no route matches the method + path combination, Hono returns 404.

Key behaviors:

  • Routes are method-specific (app.get only matches GET requests)
  • Path matching is exact unless you use wildcards or parameters
  • basePath prefixes all routes
  • Sub-routers (app.route()) add their own prefix
  • Routes must be registered before the app is exported/started

Fix 1: Register routes before exporting

The most common mistake β€” routes are added after the app is exported or the server starts.

import { Hono } from 'hono';

// ❌ Export before routes β€” routes never get registered
const app = new Hono();
export default app;
app.get('/', (c) => c.text('Hello'));  // Too late!

// βœ… Register routes BEFORE exporting
const app = new Hono();
app.get('/', (c) => c.text('Hello'));
app.get('/api/users', (c) => c.json({ users: [] }));
app.post('/api/users', async (c) => {
  const body = await c.req.json();
  return c.json({ created: true });
});
export default app;

With separate route files:

// routes/users.ts
import { Hono } from 'hono';
const users = new Hono();
users.get('/', (c) => c.json({ users: [] }));
users.post('/', async (c) => c.json({ created: true }));
export default users;

// index.ts
import { Hono } from 'hono';
import users from './routes/users';

const app = new Hono();
app.route('/api/users', users);  // Mount BEFORE export
export default app;

Fix 2: Check the basePath

If you set a basePath, ALL routes are prefixed with it. Your requests must include the prefix.

// With basePath β€” all routes are under /api
const app = new Hono().basePath('/api');
app.get('/users', handler);
// Matches: GET /api/users
// Does NOT match: GET /users

// Without basePath
const app = new Hono();
app.get('/api/users', handler);
// Matches: GET /api/users

Debugging tip: Add a root handler to confirm the basePath:

const app = new Hono().basePath('/api');
app.get('/', (c) => c.text('API root'));
// Test: curl http://localhost:8787/api/ β€” should return "API root"

Fix 3: HTTP method mismatch

Hono is strict about HTTP methods. A POST request won’t match a GET route.

app.get('/api/data', handler);   // Only matches GET
app.post('/api/data', handler);  // Only matches POST
app.put('/api/data', handler);   // Only matches PUT
app.delete('/api/data', handler); // Only matches DELETE

// Match ALL methods:
app.all('/api/data', handler);

// Match multiple specific methods:
app.on(['GET', 'POST'], '/api/data', handler);

Test with the correct method:

# GET
curl http://localhost:8787/api/data

# POST
curl -X POST -H "Content-Type: application/json" -d '{}' http://localhost:8787/api/data

# Common mistake: browser address bar only sends GET

Fix 4: Trailing slash mismatch

By default, Hono treats /api/users and /api/users/ as different routes.

app.get('/api/users', handler);
// Matches: GET /api/users
// Does NOT match: GET /api/users/  ← trailing slash!

// βœ… Handle both
app.get('/api/users', handler);
app.get('/api/users/', handler);

// βœ… Or use middleware to strip trailing slashes
import { trimTrailingSlash } from 'hono/trailing-slash';
app.use(trimTrailingSlash());

Fix 5: Cloudflare Workers β€” check wrangler.toml

If deploying to Cloudflare Workers, the issue might be in your Worker configuration:

# wrangler.toml
name = "my-api"
main = "src/index.ts"  # Must point to the file that exports your Hono app
compatibility_date = "2024-01-01"

# If using custom domains
[[routes]]
pattern = "api.example.com/*"
zone_name = "example.com"

Common issues:

  • main points to the wrong file
  • The route pattern in wrangler.toml doesn’t match your domain
  • You’re testing against the wrong URL (.workers.dev vs custom domain)

Fix 6: Wrong export format for your runtime

Different runtimes expect different export formats:

// Cloudflare Workers β€” default export
export default app;

// Bun β€” default export
export default app;

// Deno β€” use Deno.serve
Deno.serve(app.fetch);

// Node.js with @hono/node-server
import { serve } from '@hono/node-server';
serve(app);
// Don't export β€” serve() starts the server

// Node.js with Vercel
export default handle(app);
// Or for specific runtimes:
import { handle } from 'hono/vercel';
export default handle(app);

Fix 7: Middleware consuming the request

If middleware doesn’t call next(), subsequent routes never execute:

// ❌ Middleware doesn't call next β€” all routes return 404 or hang
app.use('*', async (c) => {
  console.log('Request:', c.req.url);
  // Forgot to call next()!
});

// βœ… Always call next() in middleware
app.use('*', async (c, next) => {
  console.log('Request:', c.req.url);
  await next();  // Continue to route handlers
});

Fix 8: Route parameter conflicts

Route parameters can shadow other routes if ordered incorrectly:

// ❌ :id catches everything, including "new"
app.get('/users/:id', getUser);
app.get('/users/new', createForm);  // Never reached!

// βœ… Put specific routes before parameterized ones
app.get('/users/new', createForm);
app.get('/users/:id', getUser);

Debugging: Add a catch-all to confirm Hono is running

// Add at the very end β€” after all other routes
app.notFound((c) => {
  return c.json({
    error: 'Not Found',
    method: c.req.method,
    path: c.req.path,
    message: 'No route matched. Check method and path.'
  }, 404);
});

// Or add a wildcard to see what's coming in
app.all('*', (c) => {
  return c.text(`Catch-all hit: ${c.req.method} ${c.req.path}`, 404);
});

If the catch-all responds, Hono is running but your routes don’t match. If you get no response at all, the server isn’t starting or the request isn’t reaching Hono.

FAQ

Why do my routes work locally but not on Cloudflare Workers?

Check that wrangler.toml has the correct main entry pointing to your built output. Also verify your build step produces the right file. Run wrangler dev locally to test the Worker environment.

Can I use regex in Hono routes?

Not directly. Hono uses path patterns with :param for parameters and * for wildcards. For complex matching, use middleware with custom logic.

How do I handle CORS preflight (OPTIONS) requests?

OPTIONS requests won’t match GET/POST routes. Use the CORS middleware:

import { cors } from 'hono/cors';
app.use('*', cors());

Why does my route work with curl but not from the browser?

The browser might be sending a preflight OPTIONS request (CORS), or it’s adding a trailing slash, or it’s caching a previous 404 response. Check the Network tab in DevTools for the actual request being sent.