Building Your Own Feedback API

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:

FieldTypeDescription
idUUID / intUnique identifier
post_slugstringBlog post identifier
helpfulboolean/nullYes/No vote (optional if only comment)
commentstring (text)Optional reader comment
created_attimestampAuto-generated

Later, we can extend with user info, sentiment, moderation flags, etc.

5.2 FastAPI Implementation

5.2.1 Project Setup

bash
mkdir fastapi-feedback && cd fastapi-feedback
python -m venv venv
source venv/bin/activate
pip install fastapi uvicorn sqlalchemy pydantic[dotenv] pytest

5.2.2 Database Schema (SQLite for dev)

app/models.py
1from sqlalchemy import Column, Integer, String, Boolean, DateTime
2from sqlalchemy.ext.declarative import declarative_base
3from datetime import datetime
4
5Base = declarative_base()
6
7class Feedback(Base):
8 __tablename__ = "feedback"
9
10 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.py
1from fastapi import FastAPI, Depends
2from sqlalchemy import create_engine
3from sqlalchemy.orm import sessionmaker, Session
4from app.models import Base, Feedback
5from pydantic import BaseModel
6from typing import Optional, List
7
8DATABASE_URL = "sqlite:///./feedback.db"
9
10engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
11SessionLocal = sessionmaker(bind=engine)
12
13Base.metadata.create_all(bind=engine)
14
15app = FastAPI(title="Feedback API")
16
17def get_db():
18 """Provide a database session to each request."""
19 db = SessionLocal()
20 try:
21 yield db
22 finally:
23 db.close()
24
25class FeedbackCreate(BaseModel):
26 """Schema for creating and returning feedback entries."""
27 post_slug: str
28 helpful: Optional[bool] = None
29 comment: Optional[str] = None
30
31@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 fb
39
40@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()
44
45@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

bash
uvicorn 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

bash
npx @nestjs/cli new nest-feedback
cd nest-feedback
npm install @nestjs/typeorm typeorm sqlite3

5.3.2 Feedback Entity

src/feedback/feedback.entity.ts
1import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn } from 'typeorm';
2
3@Entity()
4export class Feedback {
5 @PrimaryGeneratedColumn()
6 id: number;
7
8 @Column()
9 postSlug: string;
10
11 @Column({ nullable: true })
12 helpful?: boolean;
13
14 @Column({ nullable: true, type: 'text' })
15 comment?: string;
16
17 @CreateDateColumn()
18 createdAt: Date;
19}

5.3.3 Service ✚ Controller

src/feedback/feedback.service.ts
1import { Injectable } from '@nestjs/common';
2import { InjectRepository } from '@nestjs/typeorm';
3import { Repository } from 'typeorm';
4import { Feedback } from './feedback.entity';
5
6@Injectable()
7export class FeedbackService {
8 constructor(
9 @InjectRepository(Feedback) private repo: Repository<Feedback>,
10 ) {}
11
12 create(data: Partial<Feedback>): Promise<Feedback> {
13 const fb = this.repo.create(data);
14 return this.repo.save(fb);
15 }
16
17 findByPost(postSlug: string): Promise<Feedback[]> {
18 return this.repo.find({ where: { postSlug } });
19 }
20}
src/feedback/feedback.controller.ts
1import { Controller, Post, Get, Param, Body } from '@nestjs/common';
2import { FeedbackService } from './feedback.service';
3import { Feedback } from './feedback.entity';
4
5@Controller('feedback')
6export class FeedbackController {
7 constructor(private readonly service: FeedbackService) {}
8
9 @Post()
10 create(@Body() data: Partial<Feedback>): Promise<Feedback> {
11 return this.service.create(data);
12 }
13
14 @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.ts
1import { Module } from '@nestjs/common';
2import { TypeOrmModule } from '@nestjs/typeorm';
3import { Feedback } from './feedback.entity';
4import { FeedbackService } from './feedback.service';
5import { FeedbackController } from './feedback.controller';
6
7@Module({
8 imports: [TypeOrmModule.forFeature([Feedback])],
9 providers: [FeedbackService],
10 controllers: [FeedbackController],
11})
12export class FeedbackModule {}

And register in app.module.ts:

ts
imports: [
TypeOrmModule.forRoot({
type: 'sqlite',
database: 'feedback.db',
entities: [Feedback],
synchronize: true,
}),
FeedbackModule,
],

Run it:

bash
npm run start:dev

5.4 Frontend Widget (React)

A simple drop-in widget for either API:

src/components/FeedbackWidget.tsx
1import React, { useState } from 'react';
2
3export 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('');
9
10 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 };
20
21 return (
22 <div>
23 <p>Was this helpful?</p>
24 <button onClick={() => setHelpful(true)}>Yes</button>
25 <button onClick={() => setHelpful(false)}>No</button>
26 <textarea
27 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

  1. Spin up either the FastAPI or NestJS backend.
  2. Add the FeedbackWidget to a test blog page.
  3. Submit votes + comments and verify they’re stored in your local SQLite DB.
  4. (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.

Feedback