Recently, I just felt an urge to do something you are not supposed to do. I thought, why not add interactive elements to my supposedly completely static page. So despite this article being written in mdx, it interacts with my API via TS!
Why Go?
Go has been on my radar for a while now. Itβs fast, simple, and perfect for building small web services. Plus I thought itβd be a nice oppurtunity to learn how to create a docker image and containerize my api on my server.
Also, before writing this api I created a different project in Go called docky-go. More on that in a different blog post.
The Tech Stack
Libs used for this project are chosen on a rule-of-cool basis. Basically I picked the most popular kid on the block; so we have:
- Gin for HTTP web framework
- GORM for the database management
- PostgreSQL 15+ as the actual database (this I use quite frequently as of late)
I also used:
- JWT for auth
- Docker for deployment
Project Structure
I organized the API following a clean architecture pattern:
api/
βββ controllers/
βββ models/
βββ middleware/
βββ routes/
βββ config/
βββ main.goAuthentication System
I rigged it with explosives so do not even try to jailbreak it.
Register Endpoint
POST /api/v1/auth/register
{
"username": "user123",
"email": "user@example.com",
"password": "securepassword"
}Passwords are hashed using bcrypt before storage. Users register with the βUserβ role by default. Admin access requires database-level promotion.
Login & Logout
POST /api/v1/auth/login
{
"email": "user@example.com",
"password": "securepassword"
}
POST /api/v1/auth/logout
Headers: Authorization: Bearer {token}On successful login, the API returns a JWT token with 7-day expiry:
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": 1,
"username": "user123",
"email": "user@example.com",
"role": 0,
"created_at": "2025-12-13T22:08:56Z"
}
}Logout revokes the token by adding it to a blacklist, preventing reuse even before expiry.
Voting System
The voting system allows authenticated users to like or dislike articles. Each user can only vote once per article, but they can change their vote.
Vote Endpoints
Submit a vote: POST /api/v1/votes
{
"article_id": 1,
"vote_type": "like"
}Remove a vote: DELETE /api/v1/votes/:article_id
Get vote counts: GET /api/v1/votes/:article_id
// Response:
{
"article_id": 1",
"likes": 42,
"dislikes": 3,
"user_has_voted": true
}Security and auth
What we have so far:
- Bcrypt password hashing
- JWT token blacklist for logout
- Role-based access control (User/Admin)
- Rate limiting (100 req/s, burst 200)
- CORS configuration for allowed origins
- Security headers (XSS, HSTS, etc.)
- SQL injection protection via GORM parameterized queries
- Configurable Request size limits
Deployment
The API is containerized with Docker and deployed on my self-hosted server behind Traefik. The entire deployment is managed with Docker Compose:
services:
# PatWos - Api: my api for my portfolio
patwos-api:
container_name: patwos-api
image: ghcr.io/wosiu6/patwos-api:latest
profiles: ["apps", "all"]
restart: unless-stopped
security_opt:
- no-new-privileges:true
networks:
- default
- t3_proxy
ports:
- "${PATWOS_API_PORT}:8080"
depends_on:
- patwos-api-db
healthcheck:
disable: false
volumes:
- ./data/logs:/app/logs
- ./data/uploads:/app/uploads
- ./data/cache:/app/cache
environment:
DB_PASSWORD: ${PATWOS_API_DB_PASSWORD}
DB_HOST: ${PATWOS_API_DB_HOST}
DB_USER: ${PATWOS_API_DB_USER}
DB_NAME: ${PATWOS_API_DB_NAME}
API_PORT: ${PATWOS_API_PORT}
GIN_MODE: release
ALLOWED_ORIGINS: ${PATWOS_API_ALLOWED_ORIGINS}
MAX_REQUEST_SIZE: ${PATWOS_API_MAX_REQUEST_SIZE}
JWT_SECRET: ${PATWOS_API_JWT_SECRET}Whatβs Next?
Future Improvements
- Addin capcha or maybe email verification
- Comment reply threading
- Analytics dashboard for vote and comment statistics
- Admin dashboard perhaps?
Conclusion
Whilst it still needs some work, I am glad to see it up and running and come together like this.
Final Thoughts
Want to try it? Register an account and vote on articles! The API is live and ready to use.
Remember - no blood, no trabajo