feat: add books APIs
This commit is contained in:
@@ -0,0 +1,417 @@
|
||||
# Prabhupāda Says — BFF (Backend for Frontend)
|
||||
|
||||
## What this project is
|
||||
|
||||
A Go-based Backend for Frontend (BFF) API server for [prabhupadasays.org](https://prabhupadasays.org). It sits between the Next.js frontend (and future mobile apps) and the data layer — aggregating content from MongoDB, user data from PostgreSQL, search from Meilisearch, caching from DragonflyDB, and media from S3 into a unified API surface.
|
||||
|
||||
---
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Go 1.23** — API server
|
||||
- **Chi v5** — lightweight HTTP router with middleware support
|
||||
- **Zap** — structured logging (uber-go/zap)
|
||||
- **MongoDB** — content database (books, verses, letters, essays, glossary, disciples, transcriptions)
|
||||
- **PostgreSQL** — relational data (users, bookmarks, collections, mentions, URL shortener)
|
||||
- **Meilisearch** — typo-tolerant full-text search across all content
|
||||
- **DragonflyDB** — Redis-compatible cache (API responses, rate limiting, sessions)
|
||||
- **AWS S3** — media storage (avatars, images, audio files)
|
||||
- **FCM + APNs** — push notifications for Android and iOS
|
||||
- **JWT** — stateless authentication
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌──────────────┐ ┌─────────────────────────────────────────┐
|
||||
│ Next.js │────▶│ Go BFF API (:8080) │
|
||||
│ (frontend) │ │ │
|
||||
├──────────────┤ │ cmd/api/main.go → server.go → handlers │
|
||||
│ Mobile Apps │────▶│ │
|
||||
│ (future) │ │ ┌──────────┐ ┌───────────────┐ │
|
||||
└──────────────┘ │ │ Handlers │──│ Services │ │
|
||||
│ └──────────┘ └───────┬───────┘ │
|
||||
│ │ │
|
||||
│ ┌─────────────────────┼──────────┐ │
|
||||
│ ▼ ▼ ▼ ▼ ▼ │
|
||||
│ Mongo Postgres Meili S3 Dragonfly│
|
||||
│ (content)(users, (search)(media)(cache)│
|
||||
│ mentions, │
|
||||
│ URL short, │
|
||||
│ relations) │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
spsays-bff/
|
||||
├── cmd/
|
||||
│ └── api/
|
||||
│ └── main.go → Entry point (config, logger, server, graceful shutdown)
|
||||
├── internal/
|
||||
│ ├── config/
|
||||
│ │ └── config.go → Environment config (all services: Mongo, Postgres, Meili, S3, etc.)
|
||||
│ ├── server/
|
||||
│ │ └── server.go → Chi router setup, global middleware, route registration
|
||||
│ ├── handler/ → HTTP handlers (one file per domain)
|
||||
│ │ ├── health.go → Health check endpoint
|
||||
│ │ ├── book.go → Book routes (SB, CC, KB, BG, generic verse & prose)
|
||||
│ │ ├── letter.go → Letter routes (by year, recipient, location)
|
||||
│ │ ├── essay.go → Essay routes
|
||||
│ │ ├── glossary.go → Glossary routes (letters, terms)
|
||||
│ │ ├── disciple.go → Disciple routes (filter, search, aggregations)
|
||||
│ │ ├── journal.go → Journal routes
|
||||
│ │ ├── magazine.go → Magazine routes
|
||||
│ │ ├── other_document.go → Other-document routes
|
||||
│ │ └── transcription.go → Transcription routes (by year, place, type, slug)
|
||||
│ ├── service/ → Business logic layer (orchestrates repositories)
|
||||
│ ├── repository/ → Data access layer (Mongo, Postgres, Meili, S3, Dragonfly)
|
||||
│ │ ├── mongo.go → MongoDB connection + database handle
|
||||
│ │ ├── book_repo.go → Book / verse queries
|
||||
│ │ ├── letter_repo.go → Letter queries
|
||||
│ │ ├── essay_repo.go → Essay queries
|
||||
│ │ ├── glossary_repo.go → Glossary queries
|
||||
│ │ ├── disciple_repo.go → Disciple queries
|
||||
│ │ ├── journal_repo.go → Journal queries
|
||||
│ │ ├── magazine_repo.go → Magazine queries
|
||||
│ │ ├── other_document_repo.go → Other-document queries
|
||||
│ │ └── transcription_repo.go → Transcription queries (with contentJson fallback)
|
||||
│ ├── middleware/
|
||||
│ │ ├── logger.go → Zap-based request logging (method, path, status, duration)
|
||||
│ │ ├── auth.go → JWT Bearer token authentication + context user extraction
|
||||
│ │ └── ratelimit.go → In-memory token bucket rate limiter (→ DragonflyDB in production)
|
||||
│ └── model/
|
||||
│ ├── content.go → Content models (Book, Verse, Letter, Essay, GlossaryEntry,
|
||||
│ │ Disciple, Transcription, ContentBlock, Synonym)
|
||||
│ └── user.go → User models (User, Bookmark, Collection, Mention, ShortURL)
|
||||
├── pkg/ → Public packages (importable by other projects)
|
||||
│ ├── response/
|
||||
│ │ └── response.go → Standardized API response helpers (JSON, Error, Envelope, Meta)
|
||||
│ ├── validator/
|
||||
│ │ └── validator.go → Field validation (Required, MaxLength, Email, In, etc.)
|
||||
│ ├── push/ → Push notification sender (FCM + APNs) — placeholder
|
||||
│ └── shorturl/ → URL shortener logic — placeholder
|
||||
├── migrations/ → PostgreSQL migrations (goose or golang-migrate)
|
||||
├── go.mod → Module: github.com/prabhupadasays/spsays-bff
|
||||
├── go.sum
|
||||
├── Makefile → build, run, dev, test, clean, lint, tidy
|
||||
├── .env.example → Template for all environment variables
|
||||
└── .gitignore
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Routes
|
||||
|
||||
All routes are prefixed with `/api/v1`.
|
||||
|
||||
### Health
|
||||
|
||||
| Method | Route | Description |
|
||||
|--------|-------|-------------|
|
||||
| `GET` | `/health` | Server health check |
|
||||
|
||||
### Books
|
||||
|
||||
| Method | Route | Description |
|
||||
|--------|-------|-------------|
|
||||
| `GET` | `/api/v1/books` | List all books (lightweight summaries) |
|
||||
| `GET` | `/api/v1/books/sb` | SB book metadata |
|
||||
| `GET` | `/api/v1/books/sb/{canto}` | SB canto chapter listing |
|
||||
| `GET` | `/api/v1/books/sb/{canto}/{chapter}` | SB chapter verse index |
|
||||
| `GET` | `/api/v1/books/sb/{canto}/{chapter}/verses` | SB paginated verses |
|
||||
| `GET` | `/api/v1/books/sb/{canto}/{chapter}/{verse}` | SB single verse |
|
||||
| `GET` | `/api/v1/books/cc` | CC book metadata |
|
||||
| `GET` | `/api/v1/books/cc/{lila}` | CC līlā chapter listing |
|
||||
| `GET` | `/api/v1/books/cc/{lila}/{chapter}` | CC chapter verse index |
|
||||
| `GET` | `/api/v1/books/cc/{lila}/{chapter}/verses` | CC paginated verses |
|
||||
| `GET` | `/api/v1/books/cc/{lila}/{chapter}/{verse}` | CC single verse |
|
||||
| `GET` | `/api/v1/books/kb` | KB book metadata |
|
||||
| `GET` | `/api/v1/books/kb/{volume}` | KB volume chapter listing |
|
||||
| `GET` | `/api/v1/books/kb/{volume}/{slug}` | KB single chapter |
|
||||
| `GET` | `/api/v1/books/bg` | BG book metadata |
|
||||
| `GET` | `/api/v1/books/bg/{chapter}` | BG chapter verse index |
|
||||
| `GET` | `/api/v1/books/bg/{chapter}/verses` | BG paginated verses |
|
||||
| `GET` | `/api/v1/books/bg/{chapter}/{verse}` | BG single verse |
|
||||
| `GET` | `/api/v1/books/{code}` | Any other book metadata |
|
||||
| `GET` | `/api/v1/books/{code}/chapters` | Prose book chapter listing (no paragraphs) |
|
||||
| `GET` | `/api/v1/books/{code}/chapters/{slug}` | Prose book single chapter |
|
||||
| `GET` | `/api/v1/books/{code}/{chapter}` | Verse book chapter index |
|
||||
| `GET` | `/api/v1/books/{code}/{chapter}/verses` | Verse book paginated verses |
|
||||
| `GET` | `/api/v1/books/{code}/{chapter}/{verse}` | Verse book single verse |
|
||||
|
||||
### Letters
|
||||
|
||||
| Method | Route | Description |
|
||||
|--------|-------|-------------|
|
||||
| `GET` | `/api/v1/letters` | Total letter count |
|
||||
| `GET` | `/api/v1/letters/years` | Year → count aggregation |
|
||||
| `GET` | `/api/v1/letters/recipients` | Recipient → count aggregation |
|
||||
| `GET` | `/api/v1/letters/locations` | Location → count aggregation |
|
||||
| `GET` | `/api/v1/letters/year/{year}` | Letters for a year |
|
||||
| `GET` | `/api/v1/letters/recipient/{name}` | Letters for a recipient |
|
||||
| `GET` | `/api/v1/letters/location/{name}` | Letters from a location |
|
||||
| `GET` | `/api/v1/letters/{slug}` | Single letter |
|
||||
|
||||
### Essays
|
||||
|
||||
| Method | Route | Description |
|
||||
|--------|-------|-------------|
|
||||
| `GET` | `/api/v1/essays` | All essay summaries |
|
||||
| `GET` | `/api/v1/essays/{slug}` | Single essay |
|
||||
|
||||
### Glossary
|
||||
|
||||
| Method | Route | Description |
|
||||
|--------|-------|-------------|
|
||||
| `GET` | `/api/v1/glossary` | All glossary entries |
|
||||
| `GET` | `/api/v1/glossary/letters` | Letter → count mapping |
|
||||
| `GET` | `/api/v1/glossary/letter/{letter}` | Entries for a letter (with diacritical grouping) |
|
||||
| `GET` | `/api/v1/glossary/letter/{letter}/terms` | Term names only for sidebar |
|
||||
| `GET` | `/api/v1/glossary/term/{term}` | Single entry by term |
|
||||
|
||||
### Disciples
|
||||
|
||||
| Method | Route | Description |
|
||||
|--------|-------|-------------|
|
||||
| `GET` | `/api/v1/disciples` | Filtered list (?letter=&suffix=) |
|
||||
| `GET` | `/api/v1/disciples/count` | Total + filtered counts |
|
||||
| `GET` | `/api/v1/disciples/letters` | Letter → count aggregation |
|
||||
| `GET` | `/api/v1/disciples/suffixes` | Suffix → count aggregation |
|
||||
| `GET` | `/api/v1/disciples/search` | Search by name (?q=) |
|
||||
|
||||
### Journals
|
||||
|
||||
| Method | Route | Description |
|
||||
|--------|-------|-------------|
|
||||
| `GET` | `/api/v1/journals` | All journal metadata |
|
||||
| `GET` | `/api/v1/journals/{book}` | Single journal meta |
|
||||
| `GET` | `/api/v1/journals/{book}/chapters` | Journal chapters |
|
||||
| `GET` | `/api/v1/journals/{book}/chapters/{slug}` | Single chapter |
|
||||
|
||||
### Magazines
|
||||
|
||||
| Method | Route | Description |
|
||||
|--------|-------|-------------|
|
||||
| `GET` | `/api/v1/magazines` | All magazine metadata |
|
||||
| `GET` | `/api/v1/magazines/{book}` | Single magazine meta |
|
||||
| `GET` | `/api/v1/magazines/{book}/issues` | Magazine issues |
|
||||
| `GET` | `/api/v1/magazines/{book}/issues/{slug}` | Single issue |
|
||||
|
||||
### Other Documents
|
||||
|
||||
| Method | Route | Description |
|
||||
|--------|-------|-------------|
|
||||
| `GET` | `/api/v1/other-documents` | All document metadata |
|
||||
| `GET` | `/api/v1/other-documents/{book}` | Single document meta |
|
||||
| `GET` | `/api/v1/other-documents/{book}/chapters` | Document chapters |
|
||||
| `GET` | `/api/v1/other-documents/{book}/chapters/{slug}` | Single chapter |
|
||||
|
||||
### Transcriptions
|
||||
|
||||
| Method | Route | Description |
|
||||
|--------|-------|-------------|
|
||||
| `GET` | `/api/v1/transcriptions` | Total transcription count |
|
||||
| `GET` | `/api/v1/transcriptions/years` | Year → count aggregation |
|
||||
| `GET` | `/api/v1/transcriptions/places` | Place → count aggregation |
|
||||
| `GET` | `/api/v1/transcriptions/types` | Type distribution (`{lectures, conversations}`) |
|
||||
| `GET` | `/api/v1/transcriptions/year/{year}` | Transcriptions for a year (summaries) |
|
||||
| `GET` | `/api/v1/transcriptions/place/{place}` | Transcriptions for a place (summaries) |
|
||||
| `GET` | `/api/v1/transcriptions/type/{type}` | Transcriptions by type (`lecture` or `conversation`) |
|
||||
| `GET` | `/api/v1/transcriptions/adjacent/{slug}` | Prev/next within same year (`?year=N`) |
|
||||
| `GET` | `/api/v1/transcriptions/{slug}` | Single transcription (full content + audio URL) |
|
||||
|
||||
### Authenticated (JWT required) — planned
|
||||
|
||||
| Method | Route | Description |
|
||||
|--------|-------|-------------|
|
||||
| `POST` | `/api/v1/auth/register` | User registration |
|
||||
| `POST` | `/api/v1/auth/login` | User login (returns JWT) |
|
||||
| `POST` | `/api/v1/auth/refresh` | Refresh JWT token |
|
||||
| `GET` | `/api/v1/users/me` | Get current user profile |
|
||||
| `PUT` | `/api/v1/users/me` | Update current user profile |
|
||||
| `GET` | `/api/v1/collections` | List user's collections |
|
||||
| `POST` | `/api/v1/collections` | Create a collection |
|
||||
| `POST` | `/api/v1/collections/:id/items` | Add item to collection |
|
||||
| `DELETE` | `/api/v1/collections/:id/items/:itemId` | Remove item from collection |
|
||||
| `GET` | `/api/v1/mentions` | List user's mentions |
|
||||
| `POST` | `/api/v1/mentions` | Create a mention |
|
||||
| `POST` | `/api/v1/s` | Create a short URL |
|
||||
| `POST` | `/api/v1/notifications/register` | Register device for push notifications |
|
||||
|
||||
---
|
||||
|
||||
## Data Models
|
||||
|
||||
### Content Models (MongoDB)
|
||||
|
||||
| Model | Collection | Description |
|
||||
|-------|------------|-------------|
|
||||
| `Book` | `book_meta` | Book metadata (title, author, chapters, etc.) |
|
||||
| `Verse` | `verses` | Individual verses (BG, SB, CC, ISO, NOI, BS, MM, NBS) |
|
||||
| `Letter` | `letters` | 6,800+ letters (1947–1977) |
|
||||
| `Essay` | `essays` | 23 standalone essays |
|
||||
| `GlossaryEntry` | `glossary` | 2,749 Sanskrit/Vedic terms |
|
||||
| `Disciple` | `disciples` | 4,747 initiated disciples |
|
||||
| `Transcription` | `transcriptions` | 3,787 lectures, conversations, philosophy discussions, SB dictations |
|
||||
|
||||
### User Models (PostgreSQL)
|
||||
|
||||
| Model | Table | Description |
|
||||
|-------|-------|-------------|
|
||||
| `User` | `users` | User accounts (email, name, avatar, role) |
|
||||
| `Bookmark` | `bookmarks` | Saved content references with optional notes |
|
||||
| `Collection` | `collections` | User-created collections of bookmarks |
|
||||
| `Mention` | `mentions` | Content shared/tagged by users |
|
||||
| `ShortURL` | `short_urls` | Shortened URLs with click tracking |
|
||||
|
||||
---
|
||||
|
||||
## API Response Format
|
||||
|
||||
All responses follow a consistent envelope:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": { ... },
|
||||
"meta": {
|
||||
"page": 1,
|
||||
"limit": 20,
|
||||
"total": 150,
|
||||
"total_pages": 8
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Error responses:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": 400,
|
||||
"message": "validation failed"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Middleware Stack
|
||||
|
||||
| Middleware | Description |
|
||||
|-----------|-------------|
|
||||
| `RequestID` | Generates unique request ID (chi built-in) |
|
||||
| `RealIP` | Extracts real client IP from proxy headers |
|
||||
| `Logger` | Zap-based structured request logging |
|
||||
| `Recoverer` | Panic recovery (chi built-in) |
|
||||
| `Compress` | Gzip compression (level 5) |
|
||||
| `CORS` | Cross-origin resource sharing (frontend URL whitelisted) |
|
||||
| `Auth` | JWT Bearer token validation (applied to protected routes) |
|
||||
| `RateLimit` | Token bucket rate limiter (in-memory → DragonflyDB in production) |
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables
|
||||
|
||||
```
|
||||
# Server
|
||||
APP_ENV=development # "development" | "production"
|
||||
PORT=8080
|
||||
|
||||
# MongoDB (content)
|
||||
MONGO_URI=mongodb://... # required
|
||||
MONGO_DB=spsays
|
||||
|
||||
# PostgreSQL (users, relations)
|
||||
POSTGRES_URI=postgres://...
|
||||
|
||||
# Meilisearch
|
||||
MEILISEARCH_HOST=http://localhost:7700
|
||||
MEILISEARCH_API_KEY=
|
||||
|
||||
# DragonflyDB / Redis
|
||||
REDIS_URI=redis://localhost:6379
|
||||
|
||||
# S3
|
||||
S3_REGION=
|
||||
S3_BUCKET=
|
||||
S3_ACCESS_KEY=
|
||||
S3_SECRET_KEY=
|
||||
S3_ENDPOINT= # optional — for MinIO or S3-compatible services
|
||||
|
||||
# Push Notifications
|
||||
FCM_CREDENTIALS_FILE= # path to Firebase service account JSON
|
||||
APNS_KEY_FILE= # path to APNs auth key (.p8)
|
||||
APNS_KEY_ID=
|
||||
APNS_TEAM_ID=
|
||||
APNS_BUNDLE_ID=
|
||||
|
||||
# JWT
|
||||
JWT_SECRET= # required in production
|
||||
JWT_EXPIRATION=24h
|
||||
|
||||
# Frontend (CORS)
|
||||
FRONTEND_URL=http://localhost:3000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## How to Run
|
||||
|
||||
```bash
|
||||
# Setup
|
||||
cp .env.example .env # then fill in values
|
||||
|
||||
# Development (with live reload — requires air)
|
||||
make dev
|
||||
|
||||
# Build + run
|
||||
make run
|
||||
|
||||
# Build only
|
||||
make build
|
||||
|
||||
# Run tests
|
||||
make test
|
||||
|
||||
# Lint (requires golangci-lint)
|
||||
make lint
|
||||
|
||||
# Tidy deps
|
||||
make tidy
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Relationship to Other Projects
|
||||
|
||||
| Project | Path | Description |
|
||||
|---------|------|-------------|
|
||||
| **knowledgebase** | `../knowledgebase/` | Next.js frontend — consumes this BFF's API |
|
||||
| **extractor** | `../extractor/` | Python/TS extraction scripts + raw data + seed scripts |
|
||||
| **spsays-bff** | `./` (this project) | Go BFF API server |
|
||||
|
||||
The frontend currently fetches directly from MongoDB via Prisma. The migration plan:
|
||||
1. ✅ Done — BFF API routes matching current Prisma queries
|
||||
2. Add Meilisearch indexing + search endpoints
|
||||
3. Add PostgreSQL for user features (auth, bookmarks, collections, mentions)
|
||||
4. Switch frontend from Prisma to BFF API calls
|
||||
5. Add mobile app support (same API, push notifications)
|
||||
|
||||
---
|
||||
|
||||
## Design Principles
|
||||
|
||||
- **`internal/`** — private to this module (handlers, services, repositories, middleware, models, config)
|
||||
- **`pkg/`** — public packages that could be imported by other projects (response helpers, validators, push sender)
|
||||
- **Repository pattern** — data access isolated behind interfaces (easy to swap Mongo ↔ Postgres, mock for tests)
|
||||
- **Service layer** — business logic between handlers and repositories (orchestration, validation, caching)
|
||||
- **Handler layer** — thin HTTP handlers that parse requests, call services, return responses
|
||||
- **No ORM** — use native drivers (`mongo-go-driver`, `pgx`) for full control
|
||||
- **Explicit book routing** — SB, CC, and KB have dedicated route groups to avoid URL ambiguity with generic `{code}/{chapter}/{verse}` patterns
|
||||
Reference in New Issue
Block a user