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.getonly matches GET requests) - Path matching is exact unless you use wildcards or parameters
basePathprefixes 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:
mainpoints to the wrong file- The route pattern in
wrangler.tomldoesnβt match your domain - Youβre testing against the wrong URL (
.workers.devvs 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.
Related resources
- Express.js vs Hono vs Elysia
- Cloudflare Workers Guide
- CORS Error β Fix
- Node.js Complete Guide
- JavaScript Array Methods Cheat Sheet