Project structure

VextJS provides default directory conventions and automatic loading entries. Existing project architecture may override suggested locations. Distinguish actual Loader entries from ordinary import directories; configurable roles use their real configuration fields.

This page explains where files belong, which entries Vext loads automatically, how to organize shared code, and where development and build output goes. See the Architecture Specification for directory and dependency rules; this guide keeps concrete layouts and commands.

Standard directory structure

This tree documents the conventions Vext recognizes; it is not a promise that every optional directory is generated. vext create emits only the files with initial runtime content and never uses placeholder README files to keep empty directories in source control.

my-app/
├── public/ # Static assets copied into the frontend build
│ ├── favicon.svg # Contrast-safe V mark favicon variant
│ └── vext-mark.svg # Transparent V mark used by AppShell
│
├── src/
│ ├── frontend/ # Frontend source (default full-stack template)
│ │ ├── pages/ # Pages, layouts, error pages, and document template
│ │ │ ├── index.tsx
│ │ │ ├── layout.tsx
│ │ │ ├── _document.html
│ │ │ └── error/
│ │ │   └── default.tsx
│ │ ├── components/ # Shared components
│ │ ├── styles/ # CSS / JSCSS / tokens
│ │ │ └── index.css
│ │ ├── assets/ # Assets imported from TSX/CSS into the bundle graph
│ │ └── locales/ # Frontend page messages, grouped by feature module
│ │   └── home/
│ │     ├── zh-CN.json
│ │     └── en-US.json
│ │
│ ├── config/ # Configuration file (required)
│ │ ├── default.ts #Default configuration (must exist)
│ │ ├── bootstrap.ts # Tracked startup provider entry; generated with providers: []
│ │ ├── development.ts # Development environment coverage (optional)
│ │ ├── production.ts # Production environment coverage (optional)
│ │ └── local.ts # Generated empty local override; ignored by Git
│ │
│ ├── preload/ # Optional project-level preload sources; create when needed
│ │ └── 01-otel.ts # Execute before app configuration and initialization
│ │
│ ├── routes/ # Route definition (conventional, automatic scanning)
│ │ ├── index.ts # → /
│ │ ├── users.ts # → /users
│ │ ├── users/
│ │ │ ├── index.ts # → /users (choose one from users.ts)
│ │ │ └── [id].ts # → /users/:id (dynamic parameter)
│ │ └── admin/
│ │ ├── index.ts # → /admin
│ │ └── settings.ts # → /admin/settings
│ │
│ ├── services/ # Service layer (conventional, automatic scanning + injection)
│ │ ├── user.ts # → app.services.user
│ │ ├── order.ts # → app.services.order
│ │ └── payment/
│ │ └── stripe.ts # → app.services.payment.stripe
│ │
│ ├── jobs/ # Optional scheduled jobs; start after application readiness
│ │
│ ├── constants/ # Shared runtime values; create only for real consumers
│ │ └── services/
│ │   └── order-status.ts # Runtime constants shared by service consumers
│ │
│ ├── utils/ # Stateless reusable helpers; ordinary imports, not auto-scanned
│ │ └── format-date.ts
│ │
│ ├── middlewares/ # Middleware files resolved by configured mounted names
│ │ ├── auth.ts # → referenced by name 'auth'
│ │ └── check-role.ts # → referenced by name 'check-role'
│ │
│ ├── plugins/ # Plug-ins (conventional, automatic scanning)
│ │ ├── redis.ts # Custom plug-in
│ │ └── sentry.ts # Custom plug-in
│ │
│ ├── locales/ # Backend language packs, grouped by feature module
│ │ └── order/
│ │   ├── zh-CN.json
│ │   └── en-US.json
│ │
│ └── types/ # Application-owned declaration boundaries (TS projects)
│   ├── shared/
│   │ └── greeting.d.ts # Browser/server-safe shared contract example
│   ├── frontend/
│   │ └── home.d.ts # Frontend-only declaration example
│   ├── server/
│   │ └── services/
│   │   └── order.ts # Type-only contract shared by backend consumers
│   └── generated/ # Owned only by vext typegen
│     └── index.d.ts # Created by typegen; scaffold starts with .gitkeep
│
├── .vext/
│ ├── dev/ # Development backend compilation output
│ ├── client/ # development frontend build output
│ ├── generated/frontend/ # Generated frontend entry and registry
│ ├── types/ # hidden generated declarations
│ └── manifest/ # tooling manifests
│
├── dist/ # Build product (generated by vext build)
│ └── client/ # production frontend assets when frontend.enabled is true
├── package.json
└── tsconfig.json # TypeScript configuration

Detailed explanation of each directory

src/config/ — Configuration directory

When the framework starts, config-loader loads configuration files and merges them deeply in the following order:

Framework built-in defaults → default.ts → {profile}.ts → local.ts (development/test runtime modes only) → bootstrap provider patch → CLI override
FilePurposeIs it necessary
default.tsBasic configuration for all profiles✅ Required
bootstrap.tsTracked startup-provider entry; the scaffold starts with providers: []Optional
development.tsDefault development profile coverageOptional
production.tsDefault production profile coverageOptional
test.tsDefault test profile coverageOptional
sg-sit.tsCustom profile coverageOptional
local.tsEmpty local override generated at create time and excluded by the scaffold .gitignoreOptional

Config profiles can be selected with vext start --config <name> or VEXT_CONFIG=<name>. For example, vext start --config sg-sit loads sg-sit.ts.

Scaffolding Convention

vext create directly generates local.ts and bootstrap.ts with zero-effect defaults: an empty VextConfigOverride and providers: []. local.ts is excluded by the scaffold .gitignore, so a fresh clone may not contain it and build/start must not depend on it. bootstrap.ts is tracked normally.

// src/config/default.ts
export default {
  port: 3000,
  host: "0.0.0.0",
  logger: { level: "info" },
  openapi: { enabled: true },
};
// src/config/production.ts — only overwrite the fields that need to be changed
export default {
  logger: { level: "warn" },
  openapi: { enabled: false },
};
merge strategy

Configuration uses deep merge, you only need to declare the fields that need to be covered in the environment file. The middlewares array uses a smart patch strategy (match by name and overwrite) rather than simple array replacement. The provider patch returned by bootstrap.ts will participate in the same merge / validate / freeze process after local.ts and before CLI override.

What does bootstrap.ts do?

When the configuration must be pulled from the remote end before the application is started, src/config/bootstrap.ts can be added:

import { defineBootstrapConfig } from "vextjs";

export default defineBootstrapConfig({
  providers: [
    {
      name: "remote-config",
      async load({ env, signal }) {
        const response = await fetch(`https://config.example.com/${env}.json`, {
          signal,
        });
        return await response.json();
      },
    },
  ],
});

Common uses:

  • Database connection information
  • Nacos/Configuration Center startup patch
  • Requires infrastructure configuration that is visible before built-in plugins are initialized

src/frontend/ — Frontend directory

The default full-stack scaffold creates src/frontend/ for React page source. URL entry still lives in src/routes/**, and a route handler renders a page with res.render(page, props, options). Vext generates the browser entry and registries under .vext/generated/frontend/.

PathPurpose
pages/index.tsx / pages/index.jsxDefault page, with page id index
pages/layout.tsx / pages/layout.jsxDirectory layout, nestable and reusable
pages/error/default.tsx / pages/error/default.jsxDefault error page
pages/_document.htmlHTML document using {vext.root}, {vext.data}, {vext.entry}, and {vext.styles}
components/Shared components, importable through @components/...
styles/index.cssGlobal style entry
assets/Images, fonts, and other assets imported from TSX/CSS
locales/Frontend page copy used with useVextI18n()

When config.frontend.enabled is true:

  • vext dev builds the client into .vext/client/
  • vext build writes production assets to dist/client/
  • vext start serves the production client, SSR renderer, and static assets; unmatched HTML fallback depends on frontend.spaFallback.scopes[]

src/types/ — Application type boundaries

For a new TypeScript full-stack project, vext create emits this explicit layout:

src/types/
├── shared/
│   └── greeting.d.ts
├── frontend/
│   └── home.d.ts
└── generated/
    └── .gitkeep # replaced/supplemented by index.d.ts after vext typegen
  • src/types/shared/ is for browser/server-safe contracts that both sides may import.
  • src/types/frontend/ is for browser-facing declarations and must not import Node-only or server-only modules.
  • src/types/generated/ is framework-owned output. Do not hand-edit it; vext typegen may update its declarations.

The TypeScript API-only template creates only src/types/generated/, because it has no frontend consumer. JavaScript templates do not create src/types/. Vext deliberately does not pre-create src/types/server/: keep a server-only type next to its route, service, plugin, or Model until a real backend cross-module contract exists; teams may add a server directory later when that boundary becomes useful. A type-only contract shared by multiple backend service consumers belongs in src/types/server/services/<domain>.ts. A DTO shared with browser code belongs in src/types/shared/<domain>.ts.

Keep TypeScript runtime enums, classes, symbols, initialized constants, and other values out of src/types/**. Put a runtime value next to its single owner, or promote a value shared by multiple services to src/constants/services/<domain>.ts.

This affects only newly scaffolded projects. Existing applications are not moved, renamed, or deleted, and vext typegen continues to own only src/types/generated/; it also generates referenced declarations under .vext/types/ and related manifests. Do not hand-edit any of these outputs. Frontend and backend runtime configuration still belongs under src/config/, not under any type directory.

public/ — Frontend static assets

Files in public/ are copied into the frontend output directory. The default fullstack scaffold emits a transparent vext-mark.svg for AppShell and a contrast-safe favicon.svg; both use the same V geometry. Use public/ for those fixed-URL assets, robots files, and static images that should be served without going through the JavaScript bundle. Images or fonts imported from TSX/CSS and emitted with hashes should usually live under src/frontend/assets/.

src/routes/ — Routing directory

Route files are automatically scanned by router-loader, and file paths are directly mapped to URL prefixes. Each file uses defineRoutes() to export route definitions.

Path mapping rules

File pathURL prefixDescription
routes/index.ts/Root route
routes/users.ts/usersFirst-level routing
routes/users/index.ts/usersEquivalent to users.ts
routes/users/[id].ts/users/:idDynamic parameters
routes/admin/settings.ts/admin/settingsNested routes

Dynamic parameters

Use [paramName] syntax to represent dynamic routing parameters, which are automatically converted to :paramName when loading:

routes/users/[id].ts → /users/:id
routes/posts/[slug]/comments.ts → /posts/:slug/comments

Sub-routes within the file

Multiple sub-routes can be registered inside each file. The path will automatically be spliced with file-level prefixes:

// src/routes/users.ts → prefix /users
import { defineRoutes } from "vextjs";

export default defineRoutes((app) => {
  // GET /users/list
  app.get("/list", async (req, res) => {
    const users = await app.services.user.findAll();
    res.json(users);
  });

  // POST /users (merge with prefix when subpath is /)
  app.post("/", async (req, res) => {
    const user = await app.services.user.create(req.body);
    res.json(user, 201);
  });

  // GET /users/:id
  app.get("/:id", async (req, res) => {
    const user = await app.services.user.findById(req.params.id);
    res.json(user);
  });
});

Exclusion rules

These files are skipped rather than loaded as routes. See Actual source extensions by role for supported extensions:

  • Test files with .test. or .spec. in their names, such as *.test.ts or *.spec.js
  • TypeScript declaration files such as *.d.ts
  • Files/directories starting with _ or .
  • node_modules directory

src/services/ — Services directory

service-loader scans service files. Each default export must be a class or constructor function that can be called with new and accepts app; instances are mounted on app.services. Prefer the class form below. An ordinary arrow factory does not satisfy this construction contract.

Name mapping rules

File pathAccess methodDescription
services/user.tsapp.services.userFlat naming
services/order.tsapp.services.orderFlat naming
services/payment/stripe.tsapp.services.payment.stripeNested naming
services/user-profile.tsapp.services.userProfilekebab → camelCase

File names are automatically converted from kebab-case to camelCase. Subdirectories are mapped as nested objects.

Service support-code ownership

ContentRecommended location
Type/interface private to one serviceIn the service file or an owner-near type-only file
Type-only contract shared by backend servicessrc/types/server/services/<domain>.ts
DTO shared by server and browser codesrc/types/shared/<domain>.ts
Runtime enum/constant private to one domainNext to that domain owner
Runtime enum/constant shared by multiple services or modulessrc/constants/services/<domain>.ts

Do not place support files under src/services/_types or src/services/_enums. Runtime loading, typegen, Code Docs, and reload tooling have different consumers; keeping non-service owners outside the scanned service tree makes the boundary explicit.

Service class writing method

// src/services/user.ts
import type { VextApp } from "vextjs";

export default class UserService {
  private app: VextApp;

  constructor(app: VextApp) {
    this.app = app;
  }

  async findAll() {
    //Business logic...
    return [];
  }

  async findById(id: string) {
    // Can access other services
    // const profile = await this.app.services.userProfile.get(id);
    return { id, name: "Alice" };
  }

  async create(data: unknown) {
    this.app.logger.info({ data }, "Creating user");
    return { id: "1", ...(data as object) };
  }
}
circular dependency detection

Services are instantiated and attached one by one in filename order, not first injected in dependency topological order. A constructor that accesses a service not yet attached can fail immediately; store the app reference in the constructor and read initialized dependencies in methods.

This avoids an initialization timing problem, but does not remove circular dependencies. After loading, the framework analyzes service accesses in source, including recognizable method-body dependencies, and reports cycles. Dynamic access or inheritance may only yield an incomplete-analysis warning; that is not proof of no cycle. Extract shared logic or reverse an edge to resolve a cycle; see the Architecture Specification.

src/jobs/ — Background job entry

Scheduled jobs live in src/jobs/**, with config.jobs.dir supporting custom layouts. vext start / vext dev registers future points after readiness. Testing helpers use explicitly supplied definitions. See Jobs.

src/utils/ — Shared helper functions

Use src/utils/ only for stateless, reusable helpers with a clear boundary, such as formatting, pure calculations, or stable parsing. Import them normally; Vext does not auto-scan, instantiate, or inject src/utils/ into app.

Keep a helper next to its domain owner while it has only one consumer. Promote it to src/utils/ only after real cross-domain reuse appears. Logic that needs VextApp, a database, request context, or mutable domain state belongs near a service, plugin, or route instead. Helpers imported by frontend code must remain browser-safe; do not expose Node-only utilities through a frontend entry.

src/middlewares/ — middleware directory

middleware-loader finds files by mounted names in config.middlewares and builds a registry for route references. Placing a file in the directory does not mount it or make it global on every request. Each file exports middleware tagged by defineMiddleware or defineMiddlewareFactory.

The file name is the middleware name and is referenced by name in configuration and routing:

// src/middlewares/auth.ts
import { defineMiddleware } from "vextjs";

export default defineMiddleware(async (req, res, next) => {
  const token = req.headers["authorization"];
  if (!token) req.app.throw(401, "Unauthorized");
  // ...verify token
  await next();
});

The auth.ts fragment only shows placement and tagging, not complete identity verification; see Security for that workflow. Allowlist the middleware, then reference it from a route. The next fragment assumes a check-role factory and a business handler supplied by the app; it is not a standalone program:

// src/config/default.ts
export default {
  middlewares: [
    "auth", // Ordinary middleware
    { name: "check-role", options: { roles: ["admin"] } }, // Factory middleware + default parameters
  ],
};
// src/routes/admin.ts — referenced in routing
app.get(
  "/dashboard",
  {
    middlewares: ["auth", "check-role"],
  },
  handler,
);

See the Middleware chapter for details.

src/plugins/ — plugin directory

Plug-in files are automatically scanned by plugin-loader, topologically sorted according to dependencies statement, and then setup() is executed in sequence.

This resource-ownership sketch uses createRedisClient and app.config.redis from an application-selected Redis SDK and configuration type. They are not built-in Vext APIs. See Plugins for a complete integration.

// src/plugins/redis.ts
import { definePlugin } from "vextjs";

export default definePlugin({
  name: "redis",
  async setup(app) {
    const redis = createRedisClient(app.config.redis);
    app.extend("redis", redis);
    app.onClose(() => redis.quit());
  },
});

See the Plugins chapter for details.

src/locales/ — Internationalization directory

i18n-loader scans locale files named by language code. The current app's validator and app.throw() use the loaded messages; each app has its own locale runtime.

// src/locales/zh-CN.ts
export default {
  "user.not_found": { code: 40001, message: "用户不存在" },
  "balance.insufficient": {
    code: 20001,
    message: "余额不足,当前余额 {{balance}}",
  },
};
// src/locales/en-US.ts
export default {
  "user.not_found": { code: 40001, message: "User not found" },
  "balance.insufficient": {
    code: 20001,
    message: "Insufficient balance, current: {{balance}}",
  },
};

See the Internationalization (i18n) chapter for details.

Automatically scan the loading sequence

When the framework starts (bootstrap), each directory is loaded in the following order:

1. config/ → load and merge configuration (loadConfig)
2. locales/ → Load language pack (loadI18n)
3. plugins/ → topological sort + execute setup() (loadPlugins)
4. middlewares/ → Load named middleware from configuration (loadMiddlewares)
5. services/ → Instantiate and inject into app.services(loadServices)
6. routes/ → scan routes + register to adapter (loadRoutes)
7. frontend → Mount renderer and client assets when `frontend.enabled` is true
8. Start HTTP listening

This order ensures:

  • Configuration is ready before all modules
  • Plugins can extend the app object (e.g. inject database connections)
  • Middleware is ready before route registration
  • Services are injected before routing, and app.services can be safely accessed in the routing handler

This is the main dependency order for HTTP startup, not a full list of built-in middleware or plugin details. Frontend compilation belongs to dev/build tools. Production start consumes an existing build; it does not automatically build frontend assets.

package.json requirements

VextJS projects must be declared as ESM modules:

{
  "type": "module",
  "scripts": {
    "start": "vext start",
    "dev": "vext dev",
    "build": "vext build --typecheck"
  }
}
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "dist",
    "rootDir": "src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "noEmit": true
  },
  "include": ["src/**/*.ts", "src/**/*.tsx", ".vext/types/**/*.d.ts"],
  "exclude": ["node_modules", "dist"]
}

This is a base backend type-check configuration. Full-stack projects should retain the scaffold's JSX, DOM, and frontend alias options. The framework's build flow determines actual output; this outDir does not. noEmit controls independent type checking and does not prevent vext build.

Build product dist/

vext build maintains a source/output map for backend modules and declared JSON runtime resources. Output selection uses CLI/environment, then the last successful build record, then dist/. With frontend disabled, same-named backend directories remain included. The default output is shown below; manifests and buildId govern startup and cleanup.

dist/
├── config/
│ └── default.js
├── routes/
│ └── index.js
├── services/
│ └── user.js
├── client/
│ ├── assets/
│ ├── index.html
│ ├── manifest.json
│ └── size-report.json
└──...

:::tip development vs production

  • vext dev: TypeScript backend sources are compiled into the service's .vext/dev before its worker starts. Incremental output includes declared JSON; failures retain the previous valid runtime or explicitly request cold restart.
  • vext start: Uses the successful build record and actual backend mode. TypeScript production needs build first; merely having tsconfig does not turn a JavaScript source project into compiled mode.
    • When frontend is enabled, production start also requires client/index.html in the selected build output (normally dist/client/index.html). :::

Default roles, reusable validation and feature modules

Create these directories only when needed. Project conventions may override the suggested locations; a directory name does not add an automatic Loader.

my-app/
├── src/
│   ├── schemas/order/payment.ts          # Reusable schema; explicit export/import
│   ├── validators/order/can-pay.ts        # Business rule; called by a route/service
│   ├── models/
│   │   ├── user.ts                       # collection=users → model("users")
│   │   ├── billing/invoice.ts            # model("BillingInvoice")
│   │   └── cn/billing/invoice.ts         # model("CnBillingInvoice")
│   ├── modules/order/
│   │   ├── payment.ts                    # Feature implementation; explicit imports
│   │   └── types.ts                      # Feature-private types
│   ├── routes/orders.ts                  # Real defineRoutes registration entry
│   ├── services/order.ts                 # Injected entry delegating to modules/order
│   ├── locales/order/payment/
│   │   ├── zh-CN.json                   # Backend validation/error messages
│   │   └── en-US.json
│   └── frontend/
│       ├── hooks/                        # Explicit browser imports
│       ├── locales/order/payment/
│       │   ├── zh-CN.json               # Separate browser messages
│       │   └── en-US.json
│       └── assets/                       # Imported build assets
├── public/                               # Public files copied when frontend is enabled
├── test/
│   ├── unit/
│   ├── integration/
│   ├── e2e/                              # Real HTTP or browser tests
│   └── fixtures/                         # Test data, not production storage
└── storage/                              # Persistent user data, outside build cleanup
    ├── uploads/                          # Private unless explicitly served by the app
    └── exports/

Schemas describe structure, format and input/output contracts. Validators implement business decisions such as stock or payment eligibility. Both use ordinary imports; there is no injected app.schemas or app.validators. Keep database, network and other side effects in an explicit service/plugin. Shared browser/server modules must satisfy browser dependency boundaries; a shared directory does not make Node APIs or server credentials browser-safe.

Keep route registration in a synchronous defineRoutes factory and delegate from the handler to feature functions. Frontend page files are renderer entries; backend routes and res.render() still bind their URLs. Moving schemas to contracts/validation changes imports, without creating a new Loader configuration option.

Actual source extensions by role

RoleSupported sourceDiscovery/call boundary
routes.ts, .js, .mjsRecursive; .cjs is rejected; .mts/.cts are not route entries
services.ts, .mts, .cts, .js, .mjs, .cjsRecursive injection; index is an ordinary key segment; declarations/tests/private files excluded
config, middleware, plugin, model.ts, .js, .mjs, .cjsConfig selected by name; middleware resolved by mounted name; plugins/models follow their own scanners
preload.ts, .mts, .js, .mjsOrdered top-level src/preload entries; two populated preload source locations cannot coexist
backend locales.ts, .mts, .cts, .js, .mjs, .cjs, .jsonModule/nested paths; duplicate final keys report both sources
frontend pagesDefaults: .tsx, .jsx, .ts, .jspages.extensions is configurable; an extension still needs a supported compiler loader
types, schemas, validators, utils, modulesConsumer toolchainExplicit imports; .d.ts/.d.mts/.d.cts are declarations only

A generic module loader supporting an extension does not make every role discover it. NodeNext service declarations reference .mts/.cts as .mjs/.cjs. The framework owns the different backend deployment output mapping.

Monorepos and multiple services

workspace/
├── package.json                          # Workspaces and package-manager scripts
├── pnpm-workspace.yaml                   # Only for pnpm workspaces
├── packages/
│   ├── contracts/
│   │   ├── package.json                  # Real exports / types
│   │   ├── src/order.ts
│   │   └── dist/                         # Built by the shared package
│   └── models/
│       ├── package.json                  # Default-exported model map
│       ├── src/index.ts
│       └── dist/
└── apps/
    ├── api/
    │   ├── package.json                  # vextjs and shared-package dependencies
    │   ├── src/
    │   ├── .vext/
    │   ├── dist/
    │   └── storage/
    └── admin/
        ├── package.json                  # May use a different Vext version
        ├── src/
        ├── .vext/
        ├── dist/
        └── storage/

Build shared packages in dependency order, then run dev/build/typegen/start from each service. Vext resolves that service's installed exports, ESM/CJS conditions and declarations. It does not install dependencies or build arbitrary workspace packages. Static sourceExports do not replace executable JS or declarations.

The default backend compiler does not bundle arbitrary sibling packages through cross-root relative TS imports. Use package exports. Explicit models.dir/config/locale read roots are separate from the service's writable state. External model/locale changes use cold restart; other shared package changes require workspace build/restart orchestration. Uncached native ESM/CJS loading refreshes the entry, not every transitive module.

Services own separate business ports and build state. One real service root, including aliases, has one writer. Identical or nested output paths conflict explicitly. External outDir may use a dedicated workspace artifacts directory, but not source, another package or persistent data. Manifests identify owned outputs; do not delete another service's .vext or a shared directory wholesale.

See Database for exact model keys, whole connection overrides and sharedPackage maps, and Deployment for shared-resource isolation.

Verify directories and loading

In an existing TypeScript scaffold, merge the first src/config/default.ts example on this page and add the complete src/routes/users.ts and src/services/user.ts files. The other snippets explain their own directories; do not overwrite one configuration fragment with another. Run npm run dev from the project root, then send these Bash requests using the port shown at startup:

curl -i http://127.0.0.1:3000/users/list
curl -i http://127.0.0.1:3000/users/42
curl -i -H "Content-Type: application/json" -d '{"name":"Bob"}' http://127.0.0.1:3000/users

For the first two requests in PowerShell, use curl.exe. For JSON POST, avoid native command quoting differences with:

Invoke-WebRequest -Method Post -Uri http://127.0.0.1:3000/users -ContentType 'application/json' -Body '{"name":"Bob"}'

Expect HTTP 200, 200, and 201. The successful data values are [], { "id": "42", "name": "Alice" }, and { "id": "1", "name": "Bob" }. This in-memory example demonstrates directory mapping; it has no persistence or input validation. Add the appropriate Validation and Database responsibilities for a real endpoint.

For a 404, check the file prefix and path registered in defineRoutes. If a service is undefined, inspect its default export, naming, and exclusions. Restart after changing directory entries and repeat the requests. After dev verification, stop the dev server, run the npm run build script above (which includes type checking), then npm start and repeat all three requests. Dev requests alone do not verify deployment.

Next step