Node.js Backend Patterns for Medusa v2
Before writing code
Fetch live docs:
- Web-search
site:docs.medusajs.com api routes middlewares expressfor middleware configuration - Web-search
site:mikro-orm.io docsfor MikroORM entity and query patterns - Web-search
site:docs.medusajs.com error handling MedusaErrorfor error types - Fetch
https://docs.medusajs.com/learn/fundamentals/api-routesfor API route conventions - Web-search
site:node.org docs async_hooks performancefor Node.js async best practices
Express Middleware Chain
Medusa Request Lifecycle
Medusa v2 is built on Express. Every HTTP request flows through a defined middleware chain:
Incoming Request
├── 1. CORS middleware
├── 2. Body parser (JSON)
├── 3. Cookie parser / session
├── 4. Authentication middleware
├── 5. Custom middlewares (middlewares.ts)
├── 6. Route handler (route.ts)
└── 7. Error handler
Custom Middleware Registration
Define custom middleware in src/api/middlewares.ts:
// Fetch live docs for defineMiddlewares
// and MiddlewareRoute type signatures
import { defineMiddlewares } from "@medusajs/medusa"
| Middleware Property | Purpose |
|---|---|
matcher | Route pattern to match (glob or regex) |
method | HTTP method filter (GET, POST, etc.) |
middlewares | Array of Express middleware functions |
bodyParser | Configure or disable body parsing |
authenticate | Apply auth requirements |
Middleware Execution Order
Middleware executes in the order defined in the routes array. Global middleware (matcher *) runs before route-specific middleware.
MikroORM Entity Patterns
DML Abstraction Layer
Medusa v2 abstracts MikroORM behind DML. However, understanding the underlying ORM helps with advanced queries:
| DML Layer | MikroORM Layer | Purpose |
|---|---|---|
model.define() | @Entity() decorator | Define database entity |
| DML fields | @Property() decorators | Column definitions |
| DML relationships | @OneToMany(), @ManyToOne() | Relationship mappings |
DML .index() | @Index() decorator | Database indexes |
| Generated migrations | MikroORM migration files | Schema changes |
Query Patterns via Service Methods
Module services generated by MedusaService provide typed query methods:
| Operation | Service Method | Underlying ORM Action |
|---|---|---|
| Find many | list(filters, config) | em.find() with criteria |
| Find one | retrieve(id, config) | em.findOneOrFail() |
| Create | create(data) | em.create() + em.persist() |
| Update | update(data) | em.assign() + em.flush() |
| Delete | delete(id) | em.removeAndFlush() |
| Soft delete | softDelete(id) | Set deleted_at timestamp |
MikroORM uses the Unit of Work pattern — changes are tracked and flushed together per request.
PostgreSQL Connection Pooling
Default Connection Settings
Medusa uses MikroORM's built-in PostgreSQL driver with connection pooling:
| Setting | Default | Production Recommendation |
|---|---|---|
| Pool minimum | 2 | 5-10 |
| Pool maximum | 10 | 20-50 (based on load) |
| Idle timeout | 30s | 10-30s |
| Connection timeout | 5s | 5-10s |
| SSL mode | Disabled | require or verify-full |
Pool Management
- Configure via
DATABASE_URLquery parameters ormedusa-config.tsdatabase options - Track active vs idle connections via PostgreSQL's
pg_stat_activity - Set pool max below PostgreSQL
max_connections(reserve for admin) - Use PgBouncer for external pooling in high-concurrency scenarios
Async Patterns
Async/Await Best Practices
| Pattern | Use Case | Avoid |
|---|---|---|
async/await | Sequential async operations | Callback-based patterns |
Promise.all() | Parallel independent operations | Sequential awaits for independent tasks |
Promise.allSettled() | Parallel ops where some may fail | Promise.all() when partial success is OK |
for await...of | Async iterators, streams | Manual iterator consumption |
try/catch in async | Error handling in async functions | Unhandled promise rejections |
Medusa Workflow Steps
Workflow steps are inherently async. Each step receives context and returns a StepResponse:
// Fetch live docs for createStep async
// signature and StepResponse typing
Event-Driven Async (Subscribers)
Subscribers handle events asynchronously. They execute in the worker process when running in separate mode:
| Concept | Implementation |
|---|---|
| Event emission | Automatic on entity changes or manual via event bus |
| Subscriber registration | Export config with event and handler function |
| Execution context | Runs in worker process (async from request) |
| Error handling | Subscriber errors do not affect the triggering request |
Error Handling
Medusa Error Types
| Error Class | HTTP Status | Use Case |
|---|---|---|
MedusaError | Varies | Base error class with type codes |
NOT_FOUND | 404 | Entity not found |
INVALID_DATA | 400 | Validation failure |
UNAUTHORIZED | 401 | Authentication required |
NOT_ALLOWED | 403 | Insufficient permissions |
CONFLICT | 409 | Duplicate or conflicting state |
UNEXPECTED_STATE | 500 | Internal logic error |
Error Handling in API Routes
// Fetch live docs for MedusaError import
// and error type enum values
import { MedusaError } from "@medusajs/framework/utils"
Error Handling Strategy
- Throw
MedusaErrorwith appropriate type in service methods - Medusa's error handler middleware converts errors to HTTP responses
- Never swallow errors silently — log and re-throw or handle explicitly
- Use
try/catchin workflow steps and return compensation data on failure
Logging
Built-in Logger
Medusa provides a structured logger accessible via the DI container:
| Log Level | Method | Use Case |
|---|---|---|
error | logger.error() | Unrecoverable errors, exceptions |
warn | logger.warn() | Degraded state, deprecated usage |
info | logger.info() | Significant events, startup, shutdown |
debug | logger.debug() | Detailed diagnostic information |
Resolve the logger from the container via container.resolve("logger"). Log at appropriate levels, include contextual data (entity IDs, operation names), and configure structured JSON output in production.
Node.js Runtime Considerations
| Concern | Recommendation |
|---|---|
| Node.js version | Use Node.js 20+ (LTS) for Medusa v2 |
| Memory | Set --max-old-space-size for large catalogs |
| Event loop | Avoid blocking the event loop with synchronous operations |
| Graceful shutdown | Handle SIGTERM to close connections and finish in-flight requests |
| Clustering | Use multiple server instances (not Node cluster) behind a load balancer |
Best Practices
- Middleware discipline — define all custom middleware in
src/api/middlewares.ts; order middleware from general to specific; use route matchers to scope middleware narrowly - Database access — always go through module services, never access the entity manager directly; use
listwith filters instead of raw queries; leverage soft deletes for data preservation - Async safety — always
awaitasync operations; usePromise.allfor parallel independent work; handle errors in everytry/catchblock with meaningful error types - Performance — configure connection pool size for expected concurrency; use Redis for caching frequently accessed data; avoid N+1 queries by using service
config.relationsto eager-load relationships
Fetch the Medusa API route and middleware documentation for exact handler signatures, error types, and logging configuration before implementing.