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.
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
fetchoraxiosfor 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.