A production-inspired full-stack web application demonstrating modern software engineering practices through a scalable client-server architecture, responsive user interface, RESTful APIs, and cloud deployment.
- Live Demo
- Features
- Tech Stack
- Architecture Overview
- Folder Structure
- Database Schema
- API Documentation
- Screenshots
- Installation & Setup
- Environment Variables
- Deployment Guide
- Assumptions
- Future Improvements
- Acknowledgements
- Frontend (Vercel): https://formly-typeform-clone.vercel.app
- Backend (Render): https://formly-typeform-clone.onrender.com
- API Documentation (Swagger): https://formly-typeform-clone.onrender.com/docs
- GitHub Repository: https://github.com/agrawalanshika/Formly-Typeform-clone
| Category | Highlights |
|---|---|
| ๐ Dashboard | Create, duplicate, delete, and manage forms โข Search and sort forms โข Responsive dashboard interface |
| ๐ ๏ธ Form Builder | Drag-and-drop question reordering โข Multiple question types โข Required field support โข Question descriptions โข Live preview โข Publish/Unpublish forms |
| ๐ค Respondent Experience | One-question-at-a-time interface โข Progress indicator โข Input validation โข Shareable public links |
| ๐ Responses & Analytics | View submitted responses โข Per-question statistics โข Rating summaries โข Choice distributions |
| โ๏ธ Backend | RESTful FastAPI API โข SQLAlchemy ORM โข SQLite persistence โข Automatic seed data |
Frontend: Next.js 15 (App Router) ยท TypeScript ยท TailwindCSS ยท Framer Motion ยท React Hook Form + Zod ยท Axios ยท dnd-kit
Backend: Python ยท FastAPI ยท SQLAlchemy ยท Pydantic ยท SQLite
Deployment: Frontend โ Vercel ยท Backend โ Render ยท Database โ SQLite
The app is a classic client/server split:
- The Next.js frontend is a pure client of the API โ it never talks to the
database directly. Every page fetches through
services/*.ts(thin Axios wrappers) into typed TypeScript interfaces intypes/*.ts. - The FastAPI backend exposes a REST API. Each router handles HTTP
concerns only; anything resembling business logic (duplicating a form,
computing statistics) lives in
app/services/, keeping routers thin and readable. - The respondent flow requires no authentication โ publishing a form
generates a random
share_slugused as its public URL. A separate/api/public/forms/{share_slug}endpoint only returns forms that are actually published, so drafts are never exposed. - Creator authentication is intentionally out of scope for this assignment (per the spec) โ the app assumes a single default creator.
Browser โโ Next.js (Vercel) โโ Axios/HTTP โโ FastAPI (Render) โโ SQLAlchemy โโ SQLite
typeform-clone/
โโโ frontend/
โ โโโ app/ # Next.js App Router pages
โ โ โโโ dashboard/
โ โ โโโ builder/[formId]/
โ โ โโโ form/[formId]/ # public respondent flow (formId = share_slug)
โ โ โโโ responses/[formId]/
โ โโโ components/
โ โ โโโ ui/ # Button, Input, Badge, Modal, Toggle
โ โ โโโ builder/ # question editor, sidebar, live preview
โ โ โโโ dashboard/ # form cards, create/delete modals
โ โ โโโ public-form/ # question screen, progress bar, thank-you
โ โ โโโ responses/ # stat cards, response table/detail
โ โโโ hooks/ # useForm, useKeyboardNav
โ โโโ services/ # api.ts + typed calls per resource
โ โโโ lib/ # utils.ts (cn), validation.ts (Zod)
โ โโโ types/ # shared TS interfaces mirroring backend schemas
โ โโโ styles/ # Tailwind globals + theme tokens
โ
โโโ backend/
โ โโโ app/
โ โ โโโ routers/ # forms.py, questions.py, responses.py, stats.py
โ โ โโโ models/ # SQLAlchemy: Form, Question, QuestionOption, Response, Answer
โ โ โโโ schemas/ # Pydantic request/response shapes
โ โ โโโ services/ # form_service.py, stats_service.py
โ โ โโโ database/ # engine/session setup
โ โ โโโ main.py # app instance, CORS, router registration
โ โโโ seed.py # seeds 2 published forms + ~21 responses
โ โโโ requirements.txt
โ
โโโ README.md
Forms
โโโ id INTEGER PK
โโโ title TEXT NOT NULL
โโโ description TEXT NULL
โโโ status TEXT NOT NULL -- "draft" | "published"
โโโ share_slug TEXT UNIQUE -- public URL id, set on publish
โโโ created_at DATETIME
โโโ updated_at DATETIME
Questions
โโโ id INTEGER PK
โโโ form_id INTEGER FK -> Forms.id (CASCADE DELETE)
โโโ type TEXT NOT NULL -- short_text|long_text|multiple_choice|
โ dropdown|email|number|yes_no|rating
โโโ title TEXT NOT NULL
โโโ description TEXT NULL -- help text
โโโ placeholder TEXT NULL
โโโ is_required BOOLEAN DEFAULT FALSE
โโโ order_index INTEGER NOT NULL -- drives drag-and-drop order
โโโ rating_max INTEGER NULL -- only for type = rating
QuestionOptions -- for multiple_choice / dropdown
โโโ id INTEGER PK
โโโ question_id INTEGER FK -> Questions.id (CASCADE DELETE)
โโโ label TEXT NOT NULL
โโโ order_index INTEGER NOT NULL
Responses -- one row per submission
โโโ id INTEGER PK
โโโ form_id INTEGER FK -> Forms.id (CASCADE DELETE)
โโโ submitted_at DATETIME
โโโ is_complete BOOLEAN DEFAULT TRUE
Answers -- one row per question per response
โโโ id INTEGER PK
โโโ response_id INTEGER FK -> Responses.id (CASCADE DELETE)
โโโ question_id INTEGER FK -> Questions.id
โโโ value TEXT NOT NULL -- stored as text for every question type
Forms 1 โโโโ * Questions 1 โโโโ * QuestionOptions
โ
โโโโโ * Responses 1 โโโโ * Answers โโโโ * Questions
Answers.valueisTEXTfor every question type (numbers and choices included) instead of a nullable column per type. This keeps one simple, normalized table instead of column sprawl, and is trivial to parse back into the right type in the stats layer (stats_service.py).- Cascade deletes are used throughout (
ondelete="CASCADE"+ SQLAlchemycascade="all, delete-orphan"), so deleting a form cleanly removes its questions, options, responses, and answers. order_indexon bothQuestionsandQuestionOptionsis what powers drag-and-drop reordering in the builder; it's re-numbered contiguously whenever a question is deleted.
All endpoints are prefixed as shown. FastAPI also serves interactive docs at
/docs (Swagger UI) once the backend is running.
Forms
| Method | Path | Description |
|---|---|---|
| GET | /api/forms |
List all forms (id, title, status, response_count) |
| POST | /api/forms |
Create a form |
| GET | /api/forms/{id} |
Get a form with its ordered questions |
| PATCH | /api/forms/{id} |
Rename / update description |
| DELETE | /api/forms/{id} |
Delete a form (cascades) |
| POST | /api/forms/{id}/duplicate |
Duplicate a form + its questions |
| POST | /api/forms/{id}/publish |
Publish, generating a share_slug |
| POST | /api/forms/{id}/unpublish |
Revert to draft |
| GET | /api/public/forms/{share_slug} |
Public, no auth โ used by the respondent flow |
Questions
| Method | Path | Description |
|---|---|---|
| POST | /api/forms/{formId}/questions |
Create a question |
| PATCH | /api/questions/{id} |
Update a question |
| DELETE | /api/questions/{id} |
Delete a question |
| POST | /api/forms/{formId}/questions/reorder |
Bulk-update order_index |
Responses
| Method | Path | Description |
|---|---|---|
| POST | /api/forms/{formId}/responses |
Public โ submit a filled-out form |
| GET | /api/forms/{formId}/responses |
List all responses for a form |
| GET | /api/responses/{id} |
Get one response with all its answers |
Stats
| Method | Path | Description |
|---|---|---|
| GET | /api/forms/{formId}/stats |
Per-question choice counts / averages |
| Dashboard | Form Builder |
|---|---|
![]() |
![]() |
| Responses Dashboard | Respondent Page |
|---|---|
![]() |
![]() |
cd backend
python -m venv venv && source venv/bin/activate # optional but recommended
pip install -r requirements.txt
cp .env.example .env # adjust if needed
python seed.py # seeds 2 forms + 21 responses
uvicorn app.main:app --reload # runs on http://localhost:8000cd frontend
npm install
cp .env.example .env.local # NEXT_PUBLIC_API_URL=http://localhost:8000
npm run dev # runs on http://localhost:3000Then open http://localhost:3000 โ it redirects to /dashboard, which
already has two seeded forms ("Job Application" and "Customer Feedback")
ready to explore.
backend/.env
| Variable | Description | Default |
|---|---|---|
ALLOWED_ORIGINS |
Comma-separated frontend origins allowed by CORS | * |
frontend/.env.local
| Variable | Description | Default |
|---|---|---|
NEXT_PUBLIC_API_URL |
URL of the FastAPI backend | http://localhost:8000 |
Production: Set
NEXT_PUBLIC_API_URL=https://formly-typeform-clone.onrender.comwhen deploying to Vercel.
- Push this repo to GitHub.
- On render.com, create a New Web Service pointing
at the
backend/directory (or use the includedrender.yaml). - Build command:
pip install -r requirements.txt - Start command:
python seed.py && uvicorn app.main:app --host 0.0.0.0 --port $PORT - Set the
ALLOWED_ORIGINSenvironment variable to your Vercel URL once you have it (e.g.https://your-app.vercel.app). - Note: Render's free-tier filesystem is ephemeral โ the SQLite database
resets on every redeploy/restart. This is an accepted, documented
tradeoff for a 24-hour assignment;
seed.pyre-seeds automatically on each boot (it's a no-op if data already exists).
- Import the repo into vercel.com, set the root
directory to
frontend/. - Set the environment variable
NEXT_PUBLIC_API_URLto your deployed Render backend URL. - Deploy โ Vercel auto-detects the Next.js app.
- Creator authentication is simplified per the assignment spec โ the app assumes a single default creator with no login.
- File-upload, payments, integrations, and team collaboration are shown as "Coming soon" placeholders where relevant, as explicitly permitted by the assignment.
- SQLite is used per the required tech stack; see the Render note above regarding persistence on free-tier hosting.
The backend is hosted on Render's free tier.
If the API has been inactive for some time, the first request may take 30โ60 seconds while the server wakes up.
Bonus features, in priority order (not yet implemented):
- Export responses as CSV
- Partial response tracking / completion rate (the
is_completecolumn onResponsesalready anticipates this) - Conditional logic / branching between questions
- File upload question type
- Custom themes (colors, fonts, backgrounds)
This project was developed as part of the Scalar AI Labs SDE Internship Assignment and has been further refined for portfolio and learning purposes.



