An internal backend service for collecting, classifying, and triaging product feedback across teams and features.
Built with NestJS, GraphQL (code-first), PostgreSQL, and Prisma. Feedback classification runs asynchronously via NestJS's EventEmitter.
| Layer | Technology |
|---|---|
| Framework | NestJS (TypeScript) |
| API | GraphQL (code-first via @nestjs/graphql) |
| ORM | Prisma 4 |
| Database | PostgreSQL 15 |
| Events | @nestjs/event-emitter |
| Runtime | Node.js 16+ |
- Node.js 16+
- Docker Desktop (for PostgreSQL)
1. Install dependencies
npm install2. Start the database
docker compose up -d3. Run migrations
npm run db:migrate4. (Optional) Seed sample data
npm run db:seed5. Start the server
npm run start:devThe GraphQL endpoint is available at http://localhost:3000/graphql.
| Variable | Description |
|---|---|
DATABASE_URL |
PostgreSQL connection string (see .env) |
PORT |
HTTP port (default: 3000) |
Team ──< User ──< Feedback >──< Feature
│
Comment
│
FeedbackClassification (written async by background job)
| Entity | Key Fields |
|---|---|
Team |
id, name |
User |
id, email, teamId |
Feedback |
id, content, priority (LOW/MEDIUM/HIGH), flagged, submittedBy, assignedTeamId |
Feature |
id, name — M:N with Feedback |
Comment |
id, content, author, feedbackId |
FeedbackClassification |
id, category (BUG/FEATURE_REQUEST/UX_ISSUE/OTHER), confidence, feedbackId |
# Fetch a single feedback item with all relations
feedback(id: ID!): Feedback!
# List feedbacks with optional filters
feedbacks(
teamId: ID
flagged: Boolean
priority: Priority
category: FeedbackCategory
): [Feedback!]!
features: [Feature!]!
teams: [Team!]!
teamFeedbackSummary(teamId: ID!): TeamSummary!submitFeedback(
content: String!
submittedBy: ID!
featureIds: [ID!]!
priority: Priority!
): Feedback!
addComment(feedbackId: ID!, authorId: ID!, content: String!): Comment!
createFeature(name: String!): Feature!
createTeam(name: String!): Team!enum Priority { LOW MEDIUM HIGH }
enum FeedbackCategory { BUG FEATURE_REQUEST UX_ISSUE OTHER }
type TeamSummary {
totalFeedback: Int!
flaggedCount: Int!
topFeatures: [Feature!]!
}When submitFeedback is called:
- Feedback is saved to the database and returned immediately.
- A
FeedbackSubmittedEventis emitted with the feedback ID and content. ClassificationListenerpicks up the event, simulates ~1.5s of async processing, then writes aFeedbackClassificationrecord with a randomly assignedcategoryandconfidencescore (0.60–0.99).
Classification is available on subsequent queries to feedback(id) or feedbacks(...).
src/
app.module.ts # Root module — wires GraphQL, EventEmitter, Prisma
main.ts # Bootstrap with express.json() middleware
prisma/
prisma.module.ts # Global Prisma module
prisma.service.ts # PrismaClient with onModuleInit
common/enums/
priority.enum.ts
feedback-category.enum.ts
modules/
team/ # teams, teamFeedbackSummary, createTeam
feature/ # features, createFeature
feedback/ # feedback/feedbacks queries + submitFeedback
events/feedback-submitted.event.ts
listeners/classification.listener.ts
comment/ # addComment
user/ # User ObjectType (shared)
prisma/
schema.prisma
seed.ts
docker-compose.yml
| Script | Description |
|---|---|
npm run start |
Start server |
npm run start:dev |
Start with file watching |
npm run build |
Compile TypeScript |
npm run db:migrate |
Run Prisma migrations |
npm run db:seed |
Insert sample team, user, feature, and feedback |
Submit feedback (classification fires in background):
mutation {
submitFeedback(
content: "Search results are missing recent items"
submittedBy: "<user-id>"
featureIds: ["<feature-id>"]
priority: MEDIUM
) {
id
content
classification { category confidence }
}
}Filter flagged, high-priority feedback:
{
feedbacks(flagged: true, priority: HIGH) {
id content submittedAt
submittedBy { email }
classification { category confidence }
}
}Team summary:
{
teamFeedbackSummary(teamId: "<team-id>") {
totalFeedback
flaggedCount
topFeatures { name }
}
}