Markdown Converter
Agent skill for markdown-converter
**Architecture**: Modular Monolith with Hexagonal Architecture (Ports & Adapters)
Sign in to like and favorite skills
Architecture: Modular Monolith with Hexagonal Architecture (Ports & Adapters)
Language: Go 1.24+ with minimal dependencies (SQLite driver, bcrypt, UUID only)
Status: 75% Complete - Auth, Posts, Categories fully implemented with tests
Entry Point:
cmd/forum/main.go → cmd/forum/wire/ for all DI wiringdocs/ARCHITECTURE.md, docs/IMPLEMENTATION_ROADMAP.md, docs/UNIFIED_DI_PATTERN.md
Each module follows strict 4-directory structure:
internal/modules/{module}/ ├── domain/ # Pure business logic (stdlib only) │ ├── {entity}.go # Domain entities with Validate() │ └── errors.go # Domain-specific errors (errors.New()) ├── ports/ # Interface definitions (contracts) │ ├── service.go # INPUT PORT - Use case interface │ └── repository.go # OUTPUT PORT - Data access interface ├── application/ # Business logic orchestration │ └── service.go # Implements ports interfaces └── adapters/ # Technical implementations (FLAT - no subdirs) ├── http_handler.go # INPUT ADAPTER - HTTP handlers └── sqlite_repository.go # OUTPUT ADAPTER - Database access
Every file in
ports/ and adapters/ MUST start with:
// INPUT PORT - Service Interface// OUTPUT PORT - Repository Interface// INPUT ADAPTER - HTTP Handler// OUTPUT ADAPTER - SQLite RepositoryExample:
internal/modules/auth/ports/service.go starts with // INPUT PORT - Service Interface
domain → stdlib ONLY (no imports from project) ports → domain only application → domain, ports adapters → domain, ports (never application!)
Cross-module: Import
ports.XService interface ONLY. Never import application/ or adapters/ from other modules.
// internal/modules/{module}/domain/{entity}.go type Post struct { ID string Title string Content string CreatedAt time.Time } func (p *Post) Validate() error { if p.Title == "" { return domain.ErrEmptyTitle } return nil } // internal/modules/{module}/domain/errors.go var ErrEmptyTitle = errors.New("title cannot be empty")
// internal/modules/{module}/ports/service.go (INPUT PORT) type PostService interface { CreatePost(ctx context.Context, userID, title, content string) (*domain.Post, error) } // internal/modules/{module}/ports/repository.go (OUTPUT PORT) type PostRepository interface { Create(ctx context.Context, post *domain.Post) error }
// internal/modules/{module}/application/service.go type Service struct { repo ports.PostRepository } func (s *Service) CreatePost(ctx context.Context, userID, title, content string) (*domain.Post, error) { post := &domain.Post{ID: generateID(), Title: title, Content: content} if err := post.Validate(); err != nil { return nil, err } return post, s.repo.Create(ctx, post) }
// internal/modules/{module}/adapters/sqlite_repository.go (OUTPUT ADAPTER) type SQLiteRepository struct { db *sql.DB } func (r *SQLiteRepository) Create(ctx context.Context, post *domain.Post) error { _, err := r.db.ExecContext(ctx, "INSERT INTO posts...", post.ID, post.Title) return err } // internal/modules/{module}/adapters/http_handler.go (INPUT ADAPTER) type HTTPHandler struct { service ports.PostService } func (h *HTTPHandler) CreatePostAPI(w http.ResponseWriter, r *http.Request) { // Parse request, call h.service.CreatePost(), write JSON response }
cmd/forum/wire/ (CRITICAL - Don't Skip!)// cmd/forum/wire/repositories.go repos.Post = postAdapters.NewSQLitePostRepository(db) // cmd/forum/wire/services.go (ServiceContainer already exists) post: postApp.NewService(repos.Post, repos.Category), // cmd/forum/wire/handlers.go postHandler := postAdapters.NewHTTPHandler(services, templates) // cmd/forum/wire/app.go (in initServer) postHandler.RegisterRoutes(server.Router())
-- migrations/NNN_{module}_{description}.sql -- +migrate Up CREATE TABLE posts (id TEXT PRIMARY KEY, title TEXT NOT NULL); CREATE INDEX idx_posts_user_id ON posts(user_id); -- +migrate Down DROP INDEX idx_posts_user_id; DROP TABLE posts;
Migrations run automatically on startup via
cmd/forum/wire/app.go:initDatabase().
Every handler uses identical ServiceContainer pattern:
// cmd/forum/wire/services.go - ONE concrete container with ALL services type ServiceContainer struct { auth authPorts.AuthService // Private fields user userPorts.UserService post postPorts.PostService } func (sc *ServiceContainer) Auth() authPorts.AuthService { return sc.auth } func (sc *ServiceContainer) User() userPorts.UserService { return sc.user } // Each handler constructor: IDENTICAL signature across ALL handlers func NewHTTPHandler(services ServiceContainer, templates *template.Template) *HTTPHandler // Each handler declares minimal local interface (only what it needs) type ServiceContainer interface { Post() postPorts.PostService Auth() authPorts.AuthService // Only if needed for auth checks }
Benefits: Consistent signatures, explicit dependencies, easy mocking, no circular imports.
Reference:
docs/UNIFIED_DI_PATTERN.md, cmd/forum/wire/services.go
API → RegisterAPI, LoginAPI, CreatePostAPIPage → LoginPage, RegisterPage, PostDetailPageGroup in code: API handlers first, then Page handlers.
// 1. Domain errors (module-level, simple) // internal/modules/auth/domain/errors.go var ErrSessionExpired = errors.New("session has expired") // 2. Platform errors (structured, HTTP-aware) // internal/platform/errors/errors.go return errors.Wrap(err, errors.ErrCodeInternal, "failed to create session")
HTTP handlers map errors to status codes via
errors.HTTPStatus():
Always return JSON:
{error: "message"} for errors.
Run tests:
make test (runs scripts/tests/run_all_tests.sh)make test-coverage (generates coverage.html)
tests/ ├── unit/ # Business logic, isolated mocks │ ├── auth_test.go # Domain validation, service logic │ └── template_*_test.go # Template rendering tests ├── integration/ # Full request/response cycles ├── auth_test.go # Register, login, session flows └── post_test.go # CRUD operations with real DB
TDD Workflow:
go test ./internal/modules/auth/... -run TestRegisterAudit compliance: ALL scenarios from
docs/requirements/audit.md MUST have integration tests. DO NOT modify audit.md.
Local development:
make go # Runs with `go run cmd/forum/main.go` make build # Builds binary to bin/forum make run # Builds then runs binary
Docker:
docker-compose up # Starts forum + database make docker-build # Builds Docker image
Important: SQLite requires
CGO_ENABLED=1 (set in Makefile).
Config:
internal/platform/config/config.go - loads from env vars with defaultsinternal/platform/logger/ - structured logging (Debug, Info, Warn, Error)
lgr.Info("Starting service", logger.String("module", "auth")) lgr.Error("Failed operation", logger.Error(err), logger.String("user_id", uid))
Middleware order (in
cmd/forum/wire/app.go):
cmd/forum/main.go - Minimal lifecycle (config → wire → start → shutdown)cmd/forum/wire/{app,repositories,services,handlers}.go - All dependency injectiondocs/ARCHITECTURE.md - Design rationale, dependency rulesdocs/IMPLEMENTATION_ROADMAP.md - Progress tracking (75% complete)docs/UNIFIED_DI_PATTERN.md - ServiceContainer detailsinternal/modules/auth/ - Fully implemented example (ports → app → adapters)docs/requirements/audit.md - Authoritative test scenarios (DO NOT modify)docs/log/UNIFIED_LOGGER_IMPROVEMENT_PLAN.md - Middleware implementation TODO✅ Fully Implemented:
⚠️ Placeholder/TODO (look for
// TODO: comments):
internal/platform/httpserver/middleware.go)When implementing: Replace
// TODO: placeholders with actual logic. Follow the hexagonal pattern exactly as shown in auth/post modules.
Adding features:
docs/IMPLEMENTATION_ROADMAP.md for current prioritiesinternal/modules/auth/ as reference implementationCommon commands:
make go # Run locally (go run cmd/forum/main.go) make test # Full test suite make test-coverage # Generate coverage.html make fmt # Format code make vet # Static analysis
Testing workflow:
# Run specific module tests go test ./internal/modules/auth/... -v # Run with race detector go test -race ./... # Integration tests only go test ./tests/integration/... -v
Git commits:
[module] Brief description[auth] Implement session validation, [post] Add category filtering❌ Don't do:
adapters/ (keep flat: http_handler.go, sqlite_repository.go)// INPUT PORT - Service Interface, etc.)application/ from adapters (adapters → ports only)docs/requirements/{audit,requirements,morefeats}.md (authoritative specs)config.Config structs)✅ Do:
[OPTIONAL FEATURE] commentsDomain validation:
func (p *Post) Validate() error { if p.Title == "" { return domain.ErrEmptyTitle } if len(p.Title) > 300 { return domain.ErrTitleTooLong } if len(p.Categories) == 0 { return domain.ErrNoCategoriesSelected } return nil }
Service implementation:
func (s *Service) CreatePost(ctx context.Context, userID, title, content string, categories []string) (*domain.Post, error) { post := &domain.Post{ID: generateID(), UserID: userID, Title: title, Content: content, Categories: categories} if err := post.Validate(); err != nil { return nil, err } return post, s.postRepo.Create(ctx, post) }
HTTP handler pattern:
func (h *HTTPHandler) CreatePostAPI(w http.ResponseWriter, r *http.Request) { // 1. Parse & validate input // 2. Extract user from session (if auth required) // 3. Call service method // 4. Handle errors with appropriate status codes // 5. Return JSON response }
Repository pattern:
func (r *SQLiteRepository) Create(ctx context.Context, post *domain.Post) error { tx, _ := r.db.BeginTx(ctx, nil) defer tx.Rollback() _, err := tx.ExecContext(ctx, "INSERT INTO posts (id, title, content, user_id) VALUES (?, ?, ?, ?)", ...) return tx.Commit() }
forum/ ├── cmd/forum/ │ ├── main.go # Entry point (config → wire → run) │ └── wire/ # Dependency injection (repos → services → handlers) ├── internal/ │ ├── modules/ # Business modules (hexagonal structure) │ │ ├── auth/ # ✅ Auth (fully implemented) │ │ ├── post/ # ✅ Posts (fully implemented) │ │ ├── comment/ # ⚠️ TODO: Implement │ │ └── reaction/ # ⚠️ TODO: Implement │ └── platform/ # ✅ Technical infrastructure │ ├── config/ # Env-based config │ ├── database/ # SQLite connection + migrator │ ├── errors/ # Structured error types │ ├── httpserver/ # HTTP server + middleware │ ├── logger/ # Structured logging │ └── validator/ # Input validation ├── migrations/ # SQL migrations (auto-applied) ├── templates/ # HTML templates (Go html/template) ├── static/ # CSS, JS, uploads ├── tests/ │ ├── unit/ # Business logic tests │ └── integration/ # Full HTTP cycle tests └── docs/ ├── ARCHITECTURE.md # Design decisions ├── IMPLEMENTATION_ROADMAP.md # Progress tracking └── requirements/ # Specs (DO NOT modify)