A workflow status looks harmless in a React component. It is just a string such as in_review. The module that exports that string, however, can determine whether server dependencies become reachable from a client component.
SongHQ, a workspace for custom-song production, has two related but different lifecycles: an order moves through a seller's workflow, while each generation job has its own processing status. Its shared domain file keeps those enums in a module with no imports. Database model modules re-export the enums for server consumers.
This is a small separation that is useful well beyond music software.
Separate business progress from job progress
An order awaiting delivery is different from a generation that has completed. A song can be generated successfully while the seller is still reviewing it. A failed cover-generation attempt should not erase the order's business stage.
These are separate concepts in the domain:
export enum SongOrderStatus {
AWAITING_INFO = 'awaiting_info',
AWAITING_GENERATION = 'awaiting_generation',
IN_REVIEW = 'in_review',
AWAITING_DELIVERY = 'awaiting_delivery',
COMPLETED = 'completed',
}
export enum GenerationStatus {
PENDING = 'pending',
PROCESSING = 'processing',
COMPLETED = 'completed',
FAILED = 'failed',
}
Do not make the UI infer one lifecycle from the other. If the screen needs both, return both in its data contract. This also makes labels less ambiguous: “generation completed” does not automatically mean “customer order delivered.”
Give runtime constants a dependency-free home
A common starting point puts the enum next to the model queries:
// models/order.ts — illustrative example
import { db } from './database';
export enum OrderStatus {
IN_REVIEW = 'in_review',
}
export async function findOrder(id: string) {
// Query the database here.
}
Now a client component importing OrderStatus points at a module that also imports the database layer. Whether a build rejects this or eliminates unused code depends on the dependency graph and tooling. The architecture should not rely on that elimination working.
Move runtime constants into a leaf module instead:
// domain/order-status.ts — no imports
export enum OrderStatus {
IN_REVIEW = 'in_review',
}
// models/order.ts — server consumers can keep this path
export { OrderStatus } from '../domain/order-status';
// OrderBadge.tsx — client code uses the leaf directly
import { OrderStatus } from '../domain/order-status';
Re-exporting preserves a convenient server API. Client code must still import the dependency-free module directly; importing the server model's re-export would recreate the same dependency path.
Remember that an enum is a runtime value
import type is useful for types that disappear during compilation. It is not a replacement when the component executes an expression such as OrderStatus.IN_REVIEW: that expression needs a runtime value.
A string union plus a constant object is another valid design. The important boundary is where the runtime values live and what that module imports, not a preference for one TypeScript syntax.
A quick review checklist
- Can the shared status module be imported without bringing in a database driver, environment configuration, or server SDK?
- Do client components import that module directly?
- Are order progress and background-job progress represented separately?
- Are state transitions validated on the server? Sharing enum values does not authorize a client-requested transition.
- Does the normal application build still pass after moving the imports?
This pattern is visible in SongHQ, where the shared song-domain enums are kept separate from query modules. It is useful whenever a product has both human workflow stages and asynchronous processing: document review, media production, exports, or approval queues.
The snippets describing OrderStatus and findOrder are simplified examples; they are not complete database or authorization implementations.
Top comments (1)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.