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.