Write Base is a Go REST API for a modern blogging platform. It lets you manage articles with rich content blocks, tags, claps, and views. It uses MongoDB for storage and integrates with Gemini for optional content generation.
Highlights:
- Clean architecture (controller → usecase → repository).
- Fast endpoints via ESR-aligned MongoDB indexes and $text search (status-prefixed, language set to none).
- Auth with JWT, role-based admin operations, and DTO validation.
- Architecture
- Installation
- API Endpoints
- Data Models
- Error Handling
- Scenarios
- Environment Variables
- Performance Benchmarks
- Contributing
- License
The API follows a clean architecture pattern with the following layers:
- Controller: Handles HTTP requests and responses using the Gin framework.
- Usecase: Contains business logic and orchestrates interactions between repositories and policies.
- Repository: Manages database operations (MongoDB).
- Domain: Defines core data models, interfaces, and constants.
- Policy: Enforces business rules and validations.
- Utils: Provides helper functions like UUID and slug generation.
-
Clone the Repository
git clone <repository-url> cd write_base
-
Install Dependencies
go mod tidy
-
Set Up Environment Variables
Create a.envfile in the root directory with the following:MONGODB_URI=mongodb://localhost:27017 MONGODB_NAME=write_base JWT_SECRET=your_jwt_secret SERVER_PORT=8080 GEMINI_API_KEY=your_gemini_api_key
-
Run the Application
go run main.go
-
Build for Production
go build -o write_base ./write_base
- Most endpoints require a Bearer token:
Authorization: Bearer <JWT>. - Admin routes additionally require the user to have the admin role.
- Query parameters:
page(default 1),page_size(default 20, capped by server policy). - Responses include:
data,total,page,page_size, and sometimestotal_pages.
- JSON error format:
{ "error": "message" }with appropriate HTTP status code.
| Method | Endpoint | Description | Authentication |
|---|---|---|---|
| POST | /articles/new |
Create a new article | User |
| PUT | /articles/:id |
Update an existing article | User |
| DELETE | /articles/:id |
Soft delete an article | User |
| PATCH | /articles/:id/restore |
Restore a soft-deleted article | User |
| GET | /articles/:id |
Retrieve an article by ID | User |
| POST | /articles/:id/publish |
Publish an article | User |
| POST | /articles/:id/unpublish |
Unpublish an article | User |
| POST | /articles/:id/archive |
Archive an article | User |
| POST | /articles/:id/unarchive |
Unarchive an article | User |
| GET | /articles/:id/stats |
Get article statistics | User |
| GET | /articles/stats/all |
Get all article stats for a user | User |
| GET | /:slug |
Retrieve an article by slug | Optional |
| GET | /authors/:author_id/articles |
List articles by author | User |
| GET | /articles/trending |
List trending articles (last 7 days) | User |
| GET | /articles/new |
List newest articles | User |
| GET | /articles/popular |
List popular articles | User |
| POST | /authors/:author_id/articles/filter |
Filter articles by author | User |
| POST | /articles/filter |
Filter articles for all users | User |
| GET | /search?q=<query> |
Search articles by query | User |
| GET | /article/tags?tags=<tag1>,<tag2> |
List articles by tags | User |
| DELETE | /me/trash |
Empty user's trash | User |
| DELETE | /articles/trash/:id |
Permanently delete article from trash | User |
| POST | /generateslug |
Generate slug from title | User |
| POST | /articles/generatecontent |
Generate article content using Gemini API | User |
Admin Endpoints
| Method | Endpoint | Description | Authentication |
|---|---|---|---|
| GET | /admin/articles |
List all articles | Admin |
| DELETE | /admin/articles/:id/delete |
Hard delete an article | Admin |
| POST | /admin/articles/:id/unpublish |
Unpublish an article | Admin |
| Method | Endpoint | Description | Authentication |
|---|---|---|---|
| POST | /tags/new |
Create a new tag | User |
| GET | /tags |
List tags by status | User |
| PATCH | /tags/:id/approve |
Approve a tag | Admin |
| PATCH | /tags/:id/reject |
Reject a tag | Admin |
| DELETE | /tags/:id |
Delete a tag | Admin |
| Method | Endpoint | Description | Authentication |
|---|---|---|---|
| POST | /articles/:id/clap |
Add a clap to an article | User |
type Article struct {
ID string
Title string
Slug string
AuthorID string
ContentBlocks []ContentBlock
Excerpt string
Language string
Tags []string
Status ArticleStatus
Stats ArticleStats
Timestamps ArticleTimes
}- ContentBlock: Types include
heading,paragraph,image,code,video_embed,list,divider. - ArticleStatus:
draft,scheduled,published,archived,deleted. - ArticleStats: Tracks
ViewCountandClapCount. - ArticleTimes: Tracks
CreatedAt,UpdatedAt,PublishedAt,ArchivedAt.
type Tag struct {
ID string
Name string
Status TagStatus
CreatedBy string
CreatedAt time.Time
}- TagStatus:
pending,approved,rejected.
type Clap struct {
ID string
UserID string
ArticleID string
Count int
CreatedAt time.Time
UpdatedAt time.Time
}type View struct {
ID string
UserID string
ArticleID string
ClientIP string
CreatedAt time.Time
}Standard error format:
type Error struct {
Code string
Message string
}Common error codes:
GEN001: Internal server errorUSER001: UnauthorizedARTICLE001: Invalid article payloadARTICLE002: Article not foundCLAP001: Clap limit exceededTAG001: Tag not found
# Create article
curl -X POST http://localhost:8080/articles/new \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"title": "My First Article",
"slug": "my-first-article",
"content_blocks": [{"type": "paragraph", "order": 1, "content": {"paragraph": {"text": "Hello, world!"}}}],
"excerpt": "A brief introduction",
"language": "en",
"tags": ["tech", "intro"]
}'Response:
- Create requires valid content blocks and tags. Slug is auto-generated from title if omitted.
- Publish checks that all tags are approved; otherwise returns 400.
- Get-by-ID returns drafts to authors; for others, only if the article is published. Views are recorded for published content.
- Search uses MongoDB
$textand is limited to published content.
- Users propose tags; admins approve/reject. Unapproved tags can be used in drafts but block publish.
- Claps are rate-limited per user per article; exceeding returns HTTP 429.
- Views increment on published content reads; both anonymous (via client IP) and authenticated views are supported.
{ "data": { "id": "<article_id>" } }# Publish article
curl -X POST http://localhost:8080/articles/<article_id>/publish \
-H "Authorization: Bearer <token>"Response:
{ "data": { "id": "<article_id>", "status": "published" } }curl -X POST http://localhost:8080/authors/<author_id>/articles/filter \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"filter": {"statuses": ["published"], "tags": ["tech"]},
"pagination": {"page": 1, "page_size": 10}
}'Response:
{ "data": [...], "total": 5, "page": 1, "page_size": 10 }curl -X POST http://localhost:8080/articles/<article_id>/clap \
-H "Authorization: Bearer <token>"Response:
{ "view_count": 10, "clap_count": 5 }# List all articles
curl -X GET http://localhost:8080/admin/articles?page=1&page_size=20 \
-H "Authorization: Bearer <admin_token>"Response:
{ "data": [...], "total": 100, "page": 1, "page_size": 20 }# Hard delete article
curl -X DELETE http://localhost:8080/admin/articles/<article_id>/delete \
-H "Authorization: Bearer <admin_token>"Response:
204 No Content
| Variable | Description | Required |
|---|---|---|
MONGODB_URI |
MongoDB connection URI | Yes |
MONGODB_NAME |
MongoDB database name | Yes |
JWT_SECRET |
Secret key for JWT authentication | Yes |
SERVER_PORT |
Port for the HTTP server | Yes |
GEMINI_API_KEY |
API key for Gemini content generation | Yes |
Run tests with coverage:
go test ./... -coverprofile=coverage.outAs of 2025-08-19:
- Overall coverage (all packages): 10.2%
Core module coverage (selected packages):
- config: 100.0%
- internal/domain: 100.0%
- internal/infrastructure: 87.3%
- internal/infrastructure/utils: 64.3%
- internal/delivery/http/router: 65.3%
- internal/delivery/http/controller: 49.9%
- internal/repository: 35.3%
- internal/usecase: 25.4%
- pkg/di: 18.3%
Notes:
- “Overall” includes non-core or generated code (e.g., mocks, ai stubs, cmd), which may skew totals.
- “Core modules” focus on the HTTP layer, business logic, repositories, infra, and domain.
- Coverage was measured on Windows PowerShell; per-file totals are available via
go tool cover -html=coverage.out.
This project includes reproducible Go benchmarks for the MongoDB article repository to measure the effect of database indexing.
Hardware/OS for the runs below: Windows 11, Intel i5-1155G7. Your results will vary; use the exact commands to reproduce on your machine.
- none: No indexes (baseline). Search is expected to fail without the text index.
- text: Only the compound text index on
{status, title, excerpt}. - full: ESR-oriented indexes plus the same compound text index (default when not set).
The text index keys: { status: 1, title: "text", excerpt: "text" } with default_language = none and language_override = none to avoid unsupported language overrides on some servers.
Use a larger seed and longer benchtime to reduce noise.
# Baseline: no indexes (Search will fail, skip it if needed)
$env:BENCH_SEED_COUNT = "20000"; $env:BENCH_INDEX_MODE = "none";
go test -run=^$ ./internal/repository -bench=BenchmarkRepo_ -benchmem -benchtime=5s -count=1
# Text-only: enable Search index only
$env:BENCH_SEED_COUNT = "20000"; $env:BENCH_INDEX_MODE = "text";
go test -run=^$ ./internal/repository -bench=BenchmarkRepo_ -benchmem -benchtime=5s -count=1
# Full ESR + Text: production-like indexing
Remove-Item Env:BENCH_INDEX_MODE -ErrorAction SilentlyContinue
Remove-Item Env:BENCH_NO_INDEX -ErrorAction SilentlyContinue
$env:BENCH_SEED_COUNT = "20000";
go test -run=^$ ./internal/repository -bench=BenchmarkRepo_ -benchmem -benchtime=5s -count=1From a representative run on the machine above:
-
Full vs None
- ListByAuthor: 28.41ms → 2.37ms (~+1098% faster)
- Filter(Tags+Published): 37.20ms → 6.05ms (~+514% faster)
- Trending: 33.36ms → 3.56ms (~+838% faster)
- Popular: 28.76ms → 6.82ms (~+321% faster)
- Create: 0.165ms → 0.694ms (slower due to index maintenance)
- Search: baseline fails without text index; see Text-only vs Full below.
-
Text-only vs Full
- Search: 105.21ms (Full; $text + textScore; includes
statusequality) - Text-only mode enables Search but may be slower for other queries compared to Full due to missing ESR indexes.
- Search: 105.21ms (Full; $text + textScore; includes
Notes:
- docs/op is reported as 20 for list-style queries (page size = 20), confirming comparable work.
- Search without a text index is unsupported and will error; use Text or Full modes to measure Search.
- ESR rule: We build compound indexes to match Equality, Sort, then Range filters used by core queries.
- Examples:
(status, stats.view_count, timestamps.published_at)for Trending;(status, tags, timestamps.created_at)for tag filters;(author_id, status, timestamps.created_at)for author lists.
- Examples:
- Search uses a single text index with
statusas a prefix key to align with the equality filter (published).
These indexes drastically reduce scanned documents and improve latency for reads, at the cost of slightly slower writes (Create/Update) due to index updates.
- For tighter comparisons, run each benchmark multiple times (
-count=3) and increase-benchtime. - You can set
BENCH_SEED_COUNTto scale dataset size. - To analyze query plans, run MongoDB
explain()from the shell on representative queries.
- Fork the repository.
- Create a feature branch:
git checkout -b feature/<feature_name>
- Commit your changes:
git commit -m 'Add feature' - Push to your branch:
git push origin feature/<feature_name>
- Create a pull request.
MIT License