clockworklabs/SpacetimeDB · error · TypeError
Route conflict for
Error message
Route conflict for `${path}` What it means
SpacetimeDB's TypeScript Router is immutable: every `.get()/.post()/.put()/.delete()/.patch()/.any()` call returns a new Router and `addRoute` refuses to hold two routes that overlap. Overlap (`routesOverlap` in http_handlers.ts:113) means the same path AND a matching method, where `any()` overlaps every method and identical methods (including the same Extension verb) overlap each other. The throw happens eagerly at registration time, not at request time, so a conflicting route breaks the module build/publish.
Solutions
- Make the paths differ (change one route's path) or make the methods differ so `routesOverlap` returns false.
- Remove the `.any()` route for that path and register only the concrete methods you handle, since `any` conflicts with every verb on the same path.
- When merging or nesting routers, audit both routers' routes for the joined paths: `nest('/api', sub)` produces `/api/<sub-path>`, so check collisions against existing `/api/...` routes.
- If two handlers must serve the same path, write one handler that dispatches on `ctx.request.method` instead of registering two routes.
Example fix
// before
const router = new Router()
.get('/users', listUsers) // registered
.get('/users', listUsers2); // TypeError: Route conflict for `/users`
// after
const router = new Router()
.get('/users', listUsers)
.get('/users/:id', getUser); // distinct path Defensive patterns
Strategy: validation
Validate before calling
// Track registered routes while building the Router and assert before adding.
const seen = new Set<string>();
const add = (method: string, path: string) => {
const key = `${method} ${path}`;
const overlaps =
seen.has(`ANY ${path}`) || seen.has(`${method} ${path}`) || method === 'ANY' && [...seen].some(k => k.endsWith(` ${path}`));
if (overlaps) throw new Error(`planned route conflict: ${key}`);
seen.add(key);
};
// add('GET', '/users'); add('GET', '/users'); // fails with your own message before Router does Try / catch
Wrap the whole Router/schema construction in one try/catch during module init; on TypeError matching /Route conflict for `(.+)`/, log the captured path plus the method being registered so the conflicting pair is identifiable in build output.
Prevention
- Keep route tables in one file or one data structure (method+path+handler triples) and build the Router from it, so duplicates are greppable.
- Avoid `.any()` on paths that also have method-specific routes.
- When using nest()/merge(), write a unit test that flattens the final routes and asserts no duplicate method+path pairs.
When it happens
Trigger: Calling two route methods with the exact same path and same HTTP verb (e.g. `router.get('/users', h)` then `router.get('/users', h2)`); registering `.any('/path', h)` after any other method on `/path`; calling `router.merge(other)` or `router.nest(prefix, other)` when one of the sub-router's routes resolves to a path+method that already exists in this router (nesting joins prefix+suffix, so `/api` + `/users` can collide with an existing `/api/users`).
Common situations: Adding a new endpoint and forgetting one already exists at that path+verb; copy-pasting a route block and forgetting to change the path; using `.any()` as a catch-all and then adding specific method routes for the same path; merging routers from multiple feature modules where both define `/health`.
Related errors
- HTTP handler ' ' was exported more than once
- HTTP route for path ' ' refers to a handler that was not…
- Cannot nest router at
- cannot serialize refs without a typespace
- exporting something that is not a spacetime export
AI-assisted analysis of clockworklabs/SpacetimeDB@eddf9f5014 (2026-08-20).
Data as JSON: /api/errors/8aeb88bf6a02f42f.
Report an issue: GitHub.
Appendix: source
Thrown at crates/bindings-typescript/src/server/http_handlers.ts:357
for (const route of otherRouter.#routes) {
merged = merged.addRoute(route.method, route.path, route.handler);
}
return merged;
}
intoRoutes() {
return this.#routes.slice();
}
private addRoute(
method: MethodOrAny,
path: string,
handler: HttpHandlerExport<any>
) {
assertValidPath(path);
const candidate = { method, path, handler };
if (this.#routes.some(route => routesOverlap(route, candidate))) {
throw new TypeError(`Route conflict for \`${path}\``);
}
return new Router([...this.#routes, candidate]);
}
}
export function makeHttpHandlerExport<S extends UntypedSchemaDef>(
ctx: SchemaInner,
opts: HttpHandlerOpts | undefined,
fn: HandlerFn<S>
): HttpHandlerExport<S> {
const handlerExport: HttpHandlerExport<S> = Object.assign(
(...args: Parameters<HandlerFn<S>>) => fn(...args),
{
[httpHandlerFn]: fn,
[exportContext]: ctx,
[registerExport](ctx: SchemaInner, exportName: string) {
if (exportedHttpHandlerObjects.has(handlerExport)) {
throw new TypeError(View on GitHub (pinned to eddf9f5014)