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

  1. Make the paths differ (change one route's path) or make the methods differ so `routesOverlap` returns false.
  2. 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.
  3. 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.
  4. 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

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


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)