Express Map
Visual Route and Middleware Navigation for VS Code
Express Map is a free, open source Visual Studio Code extension that statically analyses Express.js applications and renders an interactive sidebar tree of every route, middleware layer, and template file. No running server is required. Open the project and the map appears.
The existing Express tooling ecosystem in VS Code has been largely stagnant since 2017. The available options are almost entirely snippet collections targeting Express 4, many of which have not been updated in seven or more years. The one meaningful exception is a route viewer that acknowledges a known prefix resolution bug in its own documentation and provides no middleware or template awareness. Express Map fills that gap with accurate static analysis, full prefix resolution at any depth, and a complete picture of how requests move through an Express application.
Features
-
Route Tree
Every route in the project is displayed in a sidebar tree with its HTTP method, fully resolved path (including all
app.use()prefix propagation at any depth), source file, and line number. Routes are expandable to show their middleware chain in order and the template they render. Clicking any route navigates directly to its definition. Routes are grouped by path prefix for readability. -
Middleware Chain Visualization
For each route, Express Map surfaces the complete ordered middleware stack: global middleware registered on
app, router-level middleware registered on the router, and route-level middleware passed as handler arguments. Each middleware entry shows its name if it is a named function, or its file and line number if it is anonymous. -
Template Navigation
Routes that call
res.render()are linked to the template they render. The template name appears as a child of the route in the tree and is clickable. A separate Templates group shows every template file with the routes that render it, enabling navigation from template to route or route to template. Hold Cmd (macOS) or Ctrl (Windows/Linux) and click a template name in source to open the template directly. -
CodeLens
Route metadata appears inline above each handler function in the editor:
GET /admin/users · 2 middleware · renders admin/usersClicking the CodeLens label reveals that route in the Express Map panel. As the cursor moves through a route file, the corresponding route is highlighted in the tree automatically.
-
Broken Reference Detection
Routes that call
res.render()with a template name that does not exist in the views directory are flagged three ways: a red squiggle on the call site, an entry in the Problems panel, and a Broken References group in the tree. -
Orphaned Template Detection
Template files that exist in the views directory but are never referenced by any
res.render()call are listed under an Orphaned Templates group. These are candidates for cleanup. -
Duplicate Route Detection
Multiple handlers registered for the same HTTP method and resolved path are grouped under a Duplicate Routes section with a warning indicator. Express silently uses the first match, so this detection surfaces conflicts that are otherwise invisible.
-
Async Safety Warnings
Async route handlers with no
try/catchblock are flagged with a warning. In Express 4, unhandled promise rejections crash the server. In Express 5, the framework handles this natively, so warnings are automatically suppressed when Express 5 is detected. -
Handlers wrapped in an async helper
app.get('/x', asyncHandler(async (req, res) => ...))registers a call expression rather than a function, so a wrapped route used to be read as the wrapper: reported as synchronous, and — more costly — itsres.render()calls went unrecognized whenever the response parameter was named anything butres, leaving the route with no template, no CodeLens link, and the template itself counted as orphaned. A one-argument call whose only argument is a function literal is now unwrapped and everything downstream reads the function inside it. The shape is matched rather than a list of names, soasyncHandler,catchAsync,wrapAsync,express-async-handler, and in-house equivalents are all covered; a two-argument call such aswithOptions(handler, { retries: 2 })is left alone. Wrapped routes are exempt from the async warning, since a wrapper taking a single handler exists to do something with what that handler throws. -
Copilot Integration
Express Map registers a GitHub Copilot language model tool that gives the AI a structured understanding of the full application including routes, templates, broken references, async issues, duplicates, and orphaned templates. The tool is privacy-first: it only runs when explicitly referenced in a Copilot Chat message and never sends data to the model automatically or in the background. Route paths, file structure, and template names stay local until explicitly shared. Source code content, environment variables, secrets, and runtime data are never sent.
-
Auto-Refresh
A file system watcher monitors all
.js,.ts, and.ejsfiles in the workspace. Any change triggers a debounced re-analysis at 500 ms so the tree stays current without manual refresh.
Supported Template Engines
EJS • Pug/Jade • Handlebars/HBS • Mustache • Nunjucks • Twig • Liquid • Eta
Template engine detection is automatic via app.set('view engine', ...) inspection, with a fallback to views directory scanning. No configuration is required.
Express Version Compatibility
| Feature | Express 4 | Express 5 |
|---|---|---|
| Route tree, CodeLens, template links | Yes | Yes |
{:param} optional params (path-to-regexp v8) |
No | Yes |
{*name} named wildcards |
No | Yes |
Array mount paths app.use(['/a', '/b'], fn) |
No | Yes |
| Async errors caught by framework | No (warnings shown) | Yes (warnings suppressed) |
The detected Express version is shown in the status bar tooltip.
How It Works
Express Map uses Babel's AST parser to read source files without executing them. On activation it finds the entry point via the package.json main field or common filenames (app.js, server.js, index.js), follows require() and import statements to discover all router files, detects app.set('views', ...) and app.set('view engine', ...) calls, and records every route registration with its full prefix stack resolved at any depth.
Dynamic route paths using template literals are converted to parameterized equivalents where possible. Fully dynamic paths built from variables or computed expressions are represented as [varName] in the tree.
Settings
No configuration is required and every setting defaults to existing behavior. These exist because the two diagnostics land in the Problems panel and stay there.
| Setting | Default | Purpose |
|---|---|---|
expressMap.diagnostics.brokenTemplateRef.severity | error | Broken template reference reporting |
expressMap.diagnostics.asyncErrorHandling.severity | warning | Async handler without try/catch |
expressMap.excludeDirs | [] | Directories the template walk skips |
Severities are enums rather than on/off switches, with hint as the value that earns the enum: underline in the editor, no entry in the Problems panel. Turning a diagnostic off removes it from the Problems panel and the editor but leaves the Broken References and Potential Issues tree sections intact — the panel is a place you go to look, so silencing a squiggle shouldn't also hide the inventory.
expressMap.excludeDirs takes directory names rather than globs, because names are what the walk compares as it descends. node_modules, .git, and out were previously hardcoded in two separate walks, so a build step copying compiled templates into the views tree produced duplicate and orphaned entries with no way to suppress them. Deliberately not settings: the views directory, template engine, and entry point, all discovered from the app itself.
Known Limitations
- Dynamic route paths: Template literals like
`/${section}/page`are converted to/:section/page. Fully dynamic paths (variables, computed expressions) appear as[varName]. - Dynamic render calls:
res.render(view)whereviewis a variable cannot be statically resolved. The render reference is collected via object property scanning as a best-effort. - Re-exported routers: Routers exported through index files with re-exports may not always be followed.
- Array-mounted sub-routers:
app.use(['/a', '/b'], router)is analysed once per mount path for route listing, but because file analysis is deduplicated, only the first path is used when routes inside share the same file. - No runtime analysis: Middleware conditionally registered at runtime will not be detected.
Requirements
- VS Code 1.118.0 or later
- An Express.js project with a
package.jsonin the workspace root - Entry point discovered via
package.json"main"field, or one of:app.js,server.js,index.js - Express 4 or 5, both fully supported
- No configuration needed
Installation
Search for Express Map in the VS Code Extensions panel, or install directly from the Marketplace. Source code is available on GitHub under the MIT license.