Home / Articles / Cannot Find Module: Debugging Express Imports and Route Params

This article is published in English.

Cannot Find Module: Debugging Express Imports and Route Params

A TypeScript Express weather API fails on a missing controller import. Trace paths, casing, and exports, then validate :city before calling live weather APIs.

852 words

Debugging a TypeScript Express weather API often teaches more about how a Node backend fits together than another abstract REST walkthrough.

The error

After the project was split into routes and controllers, the server failed with:

Cannot find module '../controllers/weatherController'

The message is common once a backend leaves a single file. Express is importing weatherController, and the runtime cannot resolve that module path.

What to check

Work through the failure in a fixed order instead of guessing.

Does the file exist? Under src/ the tree should include:

src/
├── controllers/
│   └── weatherController.ts

Does the folder name match exactly? Prefer controllers over controller or Controllers. Pluralization and casing both matter on case-sensitive filesystems.

Does the file name match exactly? Expect weatherController.ts, not WeatherController.ts, weathercontroller.ts, or weather-controller.ts. TypeScript module resolution treats those as different targets.

Is the import path correct? In weatherRoutes.ts the import and route wiring look like:

import { Router } from "express";
import { getWeather } from "../controllers/weatherController";
const router = Router();router.get("/:city", getWeather);export default router;

Does the controller actually export the function? A frequent miss is writing the handler without export, so the route import has nothing to bind.

import { Request, Response } from "express";
export const getWeather = (req: Request, res: Response): void => {
  const { city } = req.params;  res.json({
    city,
    temperature: 29,
    condition: "Cloudy",
    humidity: 82,
  });
};

Adding export in front of getWeather restored the server. One keyword, one resolved error.

What the project covers so far

At this checkpoint the weather service already includes several production-shaped pieces:

  • A TypeScript toolchain for the repo
  • A running Express HTTP process
  • Separated directories instead of one mega-file
  • URL routers wired to handlers
  • Handler modules for request logic
  • Path params such as :city
  • Structured JSON payloads
  • A missing-module failure fixed by checking paths and exports locally

Understanding route parameters

Exercise the route from an HTTP client such as Postman:

GET http://localhost:3000/weather/bangalore
GET http://localhost:3000/weather/mumbai
GET http://localhost:3000/weather/chennai

Each response changes the city field. The shape stays stable: temperature, condition, and humidity remain present. The city string arrives from the path segment after /weather/, exposed as req.params.city. That is the point of a route parameter: one route definition, many values substituted per request.

The next challenge: basic validation

A request like the following still succeeds with mock data:

GET /weather/123

and returns something like:

{
  "city": "123",
  "temperature": 29,
  "condition": "Cloudy",
  "humidity": 82
}

A city name should not be only digits. Reject that case instead of treating it as valid weather input.

Test the city param with /^\d+$/. On a full match, answer with 400 Bad Request rather than mock weather:

res.status(400).json({
  error: "City name must contain letters.",
});

Otherwise return the normal weather payload. Implementing the check before looking up a canned answer makes the rule stick better than pasting a finished snippet.

Quick knowledge check

A short self-quiz confirms the ideas, not just a lucky compile:

1. How does GET /weather/:city differ from GET /weather?city=bangalore? Path segments use a required route param baked into the URL pattern. Values after ? are query params and stay optional. Prefer route params when the value identifies the resource; prefer query params for filters and toggles.

2. Why use res.status(400).json() instead of just res.json()? Alone, res.json() defaults to 200 OK. Invalid input needs a status that signals failure, not only an error string in the body. Clients rely on that code.

3. Which layer owns business rules — the router, the controller, or elsewhere? Put rules in controllers. Routers only bind HTTP method and path to a handler. Validation and outbound calls sit in the controller first, then move into a service module when the app grows.

4. What does req.params.city contain for GET /weather/mumbai? The string "mumbai", exactly as typed in the path.

What’s coming next

Mock weather has done its job. The next layer is a live weather API, which implies:

  • Calling an external HTTP API
  • Environment variables (.env) so keys never sit hardcoded in source
  • Using fetch or axios for the outbound request
  • Handling timeouts and upstream failures cleanly
  • A services folder so controllers do not own the HTTP client work

The request path gains another layer:

Browser/Postman
      │
      ▼
   Routes
      │
      ▼
 Controllers
      │
      ▼
  Services
      │
      ▼
Weather API (external)

That layered shape matches many production Express apps. Building it incrementally keeps a clean rollback point when live integration breaks, instead of stuffing every concern into one file.