
Chapter Outline
Chapter 5: Building Your Own Feedback API
So far we’ve used Airtable and Giscus/Disqus as external services. That works well for prototyping — but at some point, you’ll want more control:
- Storing data in your own database.
- Customizing the API schema.
- Integrating feedback with dashboards and notifications.
In this chapter, we’ll:
- Design a feedback API schema.
- Build it in FastAPI (Python).
- Build the same in NestJS (TypeScript).
- Connect it to a simple frontend widget (a drop-in React component).
5.1 Feedback Data Model
We’ll keep the schema minimal but extensible:
| Field | Type | Description |
|---|---|---|
| id | UUID / int | Unique identifier |
| post_slug | string | Blog post identifier |
| helpful | boolean/null | Yes/No vote (optional if only comment) |
| comment | string (text) | Optional reader comment |
| created_at | timestamp | Auto-generated |
Later, we can extend with user info, sentiment, moderation flags, etc.
5.2 FastAPI Implementation
5.2.1 Project Setup
bashmkdir fastapi-feedback && cd fastapi-feedbackpython -m venv venvsource venv/bin/activatepip install fastapi uvicorn sqlalchemy pydantic[dotenv] pytest
5.2.2 Database Schema (SQLite for dev)
app/models.py1from sqlalchemy import Column, Integer, String, Boolean, DateTime2from sqlalchemy.ext.declarative import declarative_base3from datetime import datetime45Base = declarative_base()67class Feedback(Base):8 __tablename__ = "feedback"910 id = Column(Integer, primary_key=True, index=True)11 post_slug = Column(String, index=True)12 helpful = Column(Boolean, nullable=True)13 comment = Column(String, nullable=True)14 created_at = Column(DateTime, default=datetime.utcnow)
5.2.3 FastAPI App
app/main.py1from fastapi import FastAPI, Depends2from sqlalchemy import create_engine3from sqlalchemy.orm import sessionmaker, Session4from app.models import Base, Feedback5from pydantic import BaseModel6from typing import Optional, List78DATABASE_URL = "sqlite:///./feedback.db"910engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})11SessionLocal = sessionmaker(bind=engine)1213Base.metadata.create_all(bind=engine)1415app = FastAPI(title="Feedback API")1617def get_db():18 """Provide a database session to each request."""19 db = SessionLocal()20 try:21 yield db22 finally:23 db.close()2425class FeedbackCreate(BaseModel):26 """Schema for creating and returning feedback entries."""27 post_slug: str28 helpful: Optional[bool] = None29 comment: Optional[str] = None3031@app.post("/feedback")32def create_feedback(data: FeedbackCreate, db: Session = Depends(get_db)):33 """Create a new feedback entry."""34 fb = Feedback(post_slug=data.post_slug, helpful=data.helpful, comment=data.comment)35 db.add(fb)36 db.commit()37 db.refresh(fb)38 return fb3940@app.get("/feedback", response_model=List[FeedbackCreate])41def get_all_feedback(db: Session = Depends(get_db)):42 """Retrieve all feedback entries."""43 return db.query(Feedback).all()4445@app.get("/feedback/{post_slug}", response_model=List[FeedbackCreate])46def get_feedback(post_slug: str, db: Session = Depends(get_db)):47 """Retrieve feedback for a specific post."""48 return db.query(Feedback).filter(Feedback.post_slug == post_slug).all()
5.2.4 Run the API
bashuvicorn app.main:app --reload
Now you have:
POST /feedback→ create feedback.GET /feedback/{slug}→ list feedback for a post.
5.3 NestJS Implementation
5.3.1 Project Setup
bashnpx @nestjs/cli new nest-feedbackcd nest-feedbacknpm install @nestjs/typeorm typeorm sqlite3
5.3.2 Feedback Entity
src/feedback/feedback.entity.ts1import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn } from 'typeorm';23@Entity()4export class Feedback {5 @PrimaryGeneratedColumn()6 id: number;78 @Column()9 postSlug: string;1011 @Column({ nullable: true })12 helpful?: boolean;1314 @Column({ nullable: true, type: 'text' })15 comment?: string;1617 @CreateDateColumn()18 createdAt: Date;19}
5.3.3 Service ✚ Controller
src/feedback/feedback.service.ts1import { Injectable } from '@nestjs/common';2import { InjectRepository } from '@nestjs/typeorm';3import { Repository } from 'typeorm';4import { Feedback } from './feedback.entity';56@Injectable()7export class FeedbackService {8 constructor(9 @InjectRepository(Feedback) private repo: Repository<Feedback>,10 ) {}1112 create(data: Partial<Feedback>): Promise<Feedback> {13 const fb = this.repo.create(data);14 return this.repo.save(fb);15 }1617 findByPost(postSlug: string): Promise<Feedback[]> {18 return this.repo.find({ where: { postSlug } });19 }20}
src/feedback/feedback.controller.ts1import { Controller, Post, Get, Param, Body } from '@nestjs/common';2import { FeedbackService } from './feedback.service';3import { Feedback } from './feedback.entity';45@Controller('feedback')6export class FeedbackController {7 constructor(private readonly service: FeedbackService) {}89 @Post()10 create(@Body() data: Partial<Feedback>): Promise<Feedback> {11 return this.service.create(data);12 }1314 @Get(':slug')15 find(@Param('slug') slug: string): Promise<Feedback[]> {16 return this.service.findByPost(slug);17 }18}
5.3.4 Module Setup
src/feedback/feedback.module.ts1import { Module } from '@nestjs/common';2import { TypeOrmModule } from '@nestjs/typeorm';3import { Feedback } from './feedback.entity';4import { FeedbackService } from './feedback.service';5import { FeedbackController } from './feedback.controller';67@Module({8 imports: [TypeOrmModule.forFeature([Feedback])],9 providers: [FeedbackService],10 controllers: [FeedbackController],11})12export class FeedbackModule {}
And register in app.module.ts:
tsimports: [TypeOrmModule.forRoot({type: 'sqlite',database: 'feedback.db',entities: [Feedback],synchronize: true,}),FeedbackModule,],
Run it:
bashnpm run start:dev
5.4 Frontend Widget (React)
A simple drop-in widget for either API:
src/components/FeedbackWidget.tsx1import React, { useState } from 'react';23export const FeedbackWidget: React.FC<{ postSlug: string; apiUrl: string }> = ({4 postSlug,5 apiUrl,6}) => {7 const [helpful, setHelpful] = useState<boolean | null>(null);8 const [comment, setComment] = useState('');910 const submit = async () => {11 await fetch(`${apiUrl}/feedback`, {12 method: 'POST',13 headers: { 'Content-Type': 'application/json' },14 body: JSON.stringify({ post_slug: postSlug, helpful, comment }),15 });16 setHelpful(null);17 setComment('');18 alert('Thanks for your feedback!');19 };2021 return (22 <div>23 <p>Was this helpful?</p>24 <button onClick={() => setHelpful(true)}>Yes</button>25 <button onClick={() => setHelpful(false)}>No</button>26 <textarea27 value={comment}28 onChange={(e) => setComment(e.target.value)}29 placeholder="Leave a comment..."30 />31 <button onClick={submit}>Submit</button>32 </div>33 );34};
This is a bare bones component. The actual component used in your site should have styles and additional behaviors added to this component so that the user may interact with it correctly.
5.5 Summary
In this chapter, you:
- Designed a feedback schema.
- Built a FastAPI backend.
- Built the same in NestJS.
- Connected with a React widget.
Now you control your own data and can extend it however you want.
5.6 Exercise
- Spin up either the FastAPI or NestJS backend.
- Add the FeedbackWidget to a test blog page.
- Submit votes + comments and verify they’re stored in your local SQLite DB.
- (Optional) Try deploying to a free tier (Heroku, Vercel + Neon/Postgres, or Railway).
5.7 Next Step
In the next chapter, we’ll explore Scaling the Feedback System:
- Adding an analytics dashboard.
- Sending notifications (Slack, email).
- Running sentiment analysis on comments with ML.