Skip to content
HomeWorkBlogResume
Blog

Works on my machine is a bug

Three small fixes in Vidya, a learning platform monorepo, that only showed up when the code left my laptop. A phone that could not reach localhost, tests that ran twice, and Swagger forms with no fields.

November 1, 20264 min read
0

Vidya is a learning platform monorepo: an Expo student app, a NestJS API on Fastify, Postgres and a handful of shared TypeScript packages. Several people contribute to it, so it gets run on machines I have never seen and on phones I do not own.

The phone could not find localhost

In October the API worked, the web build of the student app could log in, and Expo Go on a real phone sat on the login screen and hung.

The API base URL came from EXPO_PUBLIC_API_URL, which was http://localhost:4000. On the web build, localhost is the developer's machine, so it worked. On a phone on the same Wi-Fi, localhost is the phone. The request went nowhere and nothing said so.

The fix in #20 was a small resolver in the shared API client. On native platforms it ignores a loopback URL, and falls back to the host Expo is already serving the bundle from, which by definition the phone can reach:

typescript
if (envUrl) {
  try {
    const parsed = new URL(envUrl);
    const isNative = options.platformOS === 'ios' || options.platformOS === 'android';
    if (!(isNative && isLoopbackHost(parsed.hostname))) {
      return envUrl.replace(/\/$/, '');
    }
  } catch {
    return envUrl.replace(/\/$/, '');
  }
}

const expoHost = options.expoDevHost?.trim();
if (expoHost && !isLoopbackHost(expoHost)) {
  return `http://${expoHost}:${port}`;
}

if (options.platformOS === 'android') {
  return `http://10.0.2.2:${port}`;
}

The last branch is for the Android emulator, where 10.0.2.2 is the host machine. The function is pure, so it got its own test file, and the rule is now in code instead of in someone's head.

The tests that failed only after a build

Later the API test suite started failing with one failed file and one passed, but only on machines that had already built the API. On a fresh clone it was green.

The build compiles src into dist, and tsconfig.build.json only excluded *.spec.ts. Our tests are named *.test.ts, so they were compiled too, and Vitest, with no config, collected dist/**/*.test.js as well and ran stale compiled copies next to the real ones. The fix in #26 closes both doors. The build excludes tests:

json
"exclude": [
  "**/*.spec.ts",
  "**/*.test.ts"
]

And Vitest only looks at sources:

typescript
// Only run TypeScript sources; compiled output in dist/ must never be collected as tests.
export default defineConfig({
  test: {
    include: ['src/**/*.test.ts'],
    exclude: ['dist/**', 'node_modules/**'],
  },
});

Before, 1 failed and 1 passed. After, 1 passed with 7 of 7 tests. The same PR found a second build-only bug. pnpm --dir apps/api start crashed with ERR_MODULE_NOT_FOUND, because the tsconfig sets rootDir two levels up, so the compiled entry lands at dist/apps/api/src/main.js, not dist/main.js. Nobody had noticed because everyone ran the dev script.

Swagger with empty forms

The API has Swagger at /docs, and the point of Swagger is that a frontend developer can hit "Try it out" instead of asking me. For any route with a path parameter, like /curriculum/classes/:classId/subjects, the form showed no fields and the request failed.

Nest's Swagger module does not infer @Param() names unless you use its CLI plugin, which we do not. So in #42 I added one helper and put it on every :param route in curriculum, schools, notifications and students:

typescript
/**
 * Document a path parameter. Nest Swagger does not infer `@Param()` names
 * without the CLI plugin, so every `:param` route must declare this or
 * Swagger UI shows "No parameters" / broken Try it out.
 */
export function ApiPathParam(
  name: string,
  options?: { description?: string; example?: string }
): MethodDecorator {
  return ApiParam({
    name,
    required: true,
    description: options?.description,
    example: options?.example,
    schema: { type: 'string', minLength: 1, maxLength: 120 },
  });
}

The comment is longer than the function on purpose. The next person adding a route will copy a neighbour, and the comment tells them why the decorator is there.

One command on every OS

The local setup went through two versions. The first pnpm start in #20 booted Colima and Docker. Later, in #37, I replaced the bash-only script with a Node script, scripts/start.mjs, so npm start works on macOS, Linux and Windows against a native Postgres with no Docker. A bash-only setup script quietly tells Windows contributors not to bother.

0
All posts

SreeCharan Desu

Engineer at Manuscripts · Open to remote

HomeWorkBlogResume
Chat

© 2026 SreeCharan Desu. All rights reserved.