{"record":{"id":"8aeb88bf6a02f42f","repo":"clockworklabs/SpacetimeDB","slug":"route-conflict-for-path","errorCode":null,"errorMessage":"Route conflict for `${path}`","messagePattern":"Route conflict for `(.+?)`","errorType":"exception","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"crates/bindings-typescript/src/server/http_handlers.ts","lineNumber":357,"sourceCode":"    for (const route of otherRouter.#routes) {\n      merged = merged.addRoute(route.method, route.path, route.handler);\n    }\n    return merged;\n  }\n\n  intoRoutes() {\n    return this.#routes.slice();\n  }\n\n  private addRoute(\n    method: MethodOrAny,\n    path: string,\n    handler: HttpHandlerExport<any>\n  ) {\n    assertValidPath(path);\n    const candidate = { method, path, handler };\n    if (this.#routes.some(route => routesOverlap(route, candidate))) {\n      throw new TypeError(`Route conflict for \\`${path}\\``);\n    }\n    return new Router([...this.#routes, candidate]);\n  }\n}\n\nexport function makeHttpHandlerExport<S extends UntypedSchemaDef>(\n  ctx: SchemaInner,\n  opts: HttpHandlerOpts | undefined,\n  fn: HandlerFn<S>\n): HttpHandlerExport<S> {\n  const handlerExport: HttpHandlerExport<S> = Object.assign(\n    (...args: Parameters<HandlerFn<S>>) => fn(...args),\n    {\n      [httpHandlerFn]: fn,\n      [exportContext]: ctx,\n      [registerExport](ctx: SchemaInner, exportName: string) {\n        if (exportedHttpHandlerObjects.has(handlerExport)) {\n          throw new TypeError(","sourceCodeStart":339,"sourceCodeEnd":375,"githubUrl":"https://github.com/clockworklabs/SpacetimeDB/blob/eddf9f5014579a50d4b67630e28b6e15cad9c4af/crates/bindings-typescript/src/server/http_handlers.ts#L339-L375","documentation":"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.","triggerScenarios":"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`).","commonSituations":"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`.","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."],"exampleFix":"// before\nconst router = new Router()\n  .get('/users', listUsers)   // registered\n  .get('/users', listUsers2); // TypeError: Route conflict for `/users`\n\n// after\nconst router = new Router()\n  .get('/users', listUsers)\n  .get('/users/:id', getUser); // distinct path","handlingStrategy":"validation","validationCode":"// Track registered routes while building the Router and assert before adding.\nconst seen = new Set<string>();\nconst add = (method: string, path: string) => {\n  const key = `${method} ${path}`;\n  const overlaps =\n    seen.has(`ANY ${path}`) || seen.has(`${method} ${path}`) || method === 'ANY' && [...seen].some(k => k.endsWith(` ${path}`));\n  if (overlaps) throw new Error(`planned route conflict: ${key}`);\n  seen.add(key);\n};\n// add('GET', '/users'); add('GET', '/users'); // fails with your own message before Router does","typeGuard":null,"tryCatchPattern":"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.","preventionTips":["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."],"tags":["http","routing","spacetimedb","typescript","build-time"],"backgroundTag":"route-conflict","analyzedSha":"eddf9f5014579a50d4b67630e28b6e15cad9c4af","analyzedAt":"2026-08-20T06:08:37.179Z","contentChangedAt":"2026-08-20T06:08:37.179Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}