A modern, scalable Chat Application API built with Go. It provides RESTful endpoints for user management, group messaging (with role-based access), direct messaging, message read receipts, file uploads, rate limiting, and Docker deployment.
Purpose: Backend service for a chat application.
Routes β [Middleware] β Handlers β Services β Repositories β Database (PostgreSQL/GORM)
Tests use SQLite in-memory for isolation (see Testing Notes).
| Layer | Directory | Responsibility |
|---|---|---|
| Routes | routes/routes.go |
HTTP endpoint definitions, route grouping, middleware application |
| Middleware | middleware/*.go |
Cross-cutting concerns: rate limiting, authentication (future) |
| Handlers | handler/*.go |
Request parsing, response formatting, input validation |
| Services | service/*.go |
Business logic, validation rules, authorization checks |
| Repositories | repository/*.go |
Data access, CRUD operations via GORM |
| Models | models/*.go |
GORM entity definitions, domain types |
| DB | db/db.go |
Database connection, auto-migration setup |
All layers are wired together in main.go using constructor injection:
dbConn := db.InitDB()
userRepo := repository.NewUserRepository(dbConn)
groupRepo := repository.NewGroupRepository(dbConn)
msgRepo := repository.NewMessageRepository(dbConn)
fileRepo := repository.NewFileRepository(dbConn)
userSvc := service.NewUserService(userRepo)
groupSvc := service.NewGroupService(groupRepo)
msgSvc := service.NewMessageService(msgRepo, groupRepo)
fileSvc := service.NewFileService(fileRepo, "./uploads")
userHandler := handler.NewUserHandler(userSvc)
groupHandler := handler.NewGroupHandler(groupSvc)
messageHandler := handler.NewMessageHandler(msgSvc, fileSvc)
fileHandler := handler.NewFileHandler(fileSvc)
r := routes.SetupRouter(userHandler, groupHandler, messageHandler, fileHandler)| Technology | Version | Purpose |
|---|---|---|
| Go | 1.22 | Language |
| Gin | v1.10.0 | HTTP web framework |
| GORM | v1.25.7 | ORM |
| PostgreSQL | (via gorm.io/driver/postgres v1.5.7) | Production database |
| SQLite | (via gorm.io/driver/sqlite v1.5.7) | Test database (in-memory) |
| Docker | β | Containerization |
| testify | v1.9.0 | Test assertions + mocks |
chat/
βββ main.go # Entry point β wires deps, starts server with graceful shutdown
βββ go.mod # Module: "chat", Go 1.22, PostgreSQL + SQLite drivers
βββ go.sum # Dependency checksums
βββ AGENT.md # AI agent guide (untracked)
βββ STRUCTURE.md # This file β detailed project documentation
βββ README.md # Full documentation
βββ postman_collection.json # Postman collection for testing
βββ .golangci.yml # Lint configuration
βββ .gitignore # Ignores .idea/, chat.db, uploads/
β
βββ Dockerfile # Multi-stage build (golang:1.22-alpine β alpine:3.19)
βββ docker-compose.yml # PostgreSQL 16 + app services with healthcheck
βββ .dockerignore # Build context exclusions
β
βββ models/ # GORM entity models
β βββ user.go # User struct
β βββ group.go # Group + GroupMember structs (roles: owner/admin/member)
β βββ message.go # Message + Seen + File structs (types: text/image/file)
β
βββ db/
β βββ db.go # PostgreSQL with env-based config, AutoMigrate all models
β
βββ middleware/ # Cross-cutting Gin middleware
β βββ rate_limiter.go # Per-IP sliding window rate limiter (100 req/min general, 10 req/min upload)
β βββ rate_limiter_test.go # 6 test cases for rate limiter
β
βββ repository/ # Data access layer (interfaces + implementations)
β βββ user_repository.go # UserRepository
β βββ group_repository.go # GroupRepository (includes IsMember, GetMemberRole)
β βββ message_repository.go # MessageRepository
β βββ file_repository.go # FileRepository (CreateFile, GetFileByID)
β βββ setup_test.go # SQLite in-memory test DB helper
β βββ user_repository_test.go
β βββ group_repository_test.go
β βββ message_repository_test.go
β
βββ service/ # Business logic layer (interfaces + implementations)
β βββ user_service.go # UserService
β βββ group_service.go # GroupService (role-based authorization checks)
β βββ message_service.go # MessageService (group membership validation)
β βββ file_service.go # FileService (disk storage + metadata)
β βββ mock_repository_test.go # Mock repos for service tests
β βββ user_service_test.go
β βββ group_service_test.go
β βββ message_service_test.go
β
βββ handler/ # HTTP handlers (Gin context)
β βββ user_handler.go # UserHandler β POST/GET /users, GET /users/:id
β βββ group_handler.go # GroupHandler β CRUD + member mgmt (requester_id checks, role validation)
β βββ message_handler.go # MessageHandler β send/get/delete/seen/unseen + file upload (type/content validation)
β βββ file_handler.go # FileHandler β POST /upload (multipart, 50MB max)
β βββ mock_service_test.go # Mock services for handler tests
β βββ user_handler_test.go
β βββ group_handler_test.go
β βββ message_handler_test.go
β βββ file_handler_test.go
β
βββ routes/
βββ routes.go # SetupRouter() β defines all routes, rate limiter middleware, static file serving
| Field | Type | Constraints |
|---|---|---|
| id | uint | PK, auto-increment |
| username | string | size:255, NOT NULL, UNIQUE |
| string | size:255, NOT NULL, UNIQUE | |
| created_at | time.Time | auto |
| updated_at | time.Time | auto |
| deleted_at | gorm.DeletedAt | soft delete |
| Field | Type | Constraints |
|---|---|---|
| id | uint | PK, auto-increment |
| name | string | size:255, NOT NULL, UNIQUE |
| created_at/updated_at/deleted_at | β | standard GORM timestamps |
Relations: HasMany Messages (GroupID FK), HasMany GroupMembers
| Field | Type | Constraints |
|---|---|---|
| group_id | uint | PK (composite) |
| user_id | uint | PK (composite) |
| role | string | size:50, NOT NULL, default:'member' (owner|admin|member) |
| created_at/updated_at | time.Time | auto |
| Field | Type | Constraints |
|---|---|---|
| id | uint | PK |
| sender_id | uint | NOT NULL, FK β User |
| receiver_id | *uint | nullable β direct message target |
| group_id | *uint | nullable β group message target |
| type | MessageType (string) | size:50, NOT NULL (text|image|file) |
| content | string | type:text |
| file_id | *uint | nullable, FK β File |
| created_at/updated_at/deleted_at | β | standard GORM timestamps |
Constraint: Exactly one of receiver_id or group_id must be set (validated in service layer).
| Field | Type | Constraints |
|---|---|---|
| message_id | uint | PK (composite), FK β Message |
| user_id | uint | PK (composite), FK β User |
| seen_at | time.Time | autoCreateTime |
| Field | Type | Constraints |
|---|---|---|
| id | uint | PK |
| url | string | size:512, NOT NULL |
| size | int64 | nullable |
| type | string | size:100, nullable |
| created_at/updated_at/deleted_at | β | standard GORM timestamps |
Base URL: http://localhost:8080
| Method | Path | Description |
|---|---|---|
| GET | /health |
Returns {"status": "ok"} |
| Method | Path | Description |
|---|---|---|
| POST | /users |
Create user (body: username, email) |
| GET | /users |
List all users |
| GET | /users/:id |
Get user by ID |
| Method | Path | Description |
|---|---|---|
| GET | /groups |
List all groups |
| GET | /groups/:id |
Get group by ID |
| POST | /groups |
Create group (body: name) |
| PUT | /groups/:id |
Update group name (body: name, requester_id; owner only) |
| DELETE | /groups/:id |
Soft-delete group + its members (body: requester_id; owner only) |
| POST | /groups/:id/members |
Add member (body: requester_id, user_id, role optional; admin/owner only) |
| PATCH | /groups/:id/members/:user_id |
Update member role (body: role, requester_id; admin/owner only, cannot change owner) |
| DELETE | /groups/:id/members/:user_id |
Remove member (body: requester_id; admin/owner only, cannot remove owner) |
| GET | /groups/:id/members |
List group members |
| POST | /groups/:id/leave |
Leave group (body: user_id; owner cannot leave) |
| Method | Path | Description |
|---|---|---|
| POST | /messages |
Send message (body: sender_id, receiver_id or group_id, type, content, file_id) |
| POST | /messages/upload |
Upload file + send message in one step (multipart: file, sender_id, receiver_id/group_id, content) |
| GET | /messages |
Get conversation (query: receiver_id or group_id, limit default 50, offset default 0) |
| PUT | /messages/:id |
Edit message (body: sender_id, content; can only edit own) |
| POST | /messages/:id/seen |
Mark message as seen (body: user_id) |
| DELETE | /messages/:id |
Soft-delete message |
| GET | /messages/unseen/:user_id |
Get unseen counts grouped by sender + group |
| Method | Path | Description |
|---|---|---|
| GET | /conversations?user_id= |
List all conversations with latest message + unseen count |
| Method | Path | Description |
|---|---|---|
| POST | /upload |
Upload file (multipart: file, max 50MB). Returns file metadata with id, url, size, type |
| GET | /uploads/*path |
Static file serving (serves uploaded files) |
{
"direct_messages": [
{ "user_id": 1, "username": "john_doe", "unseen_count": 5 }
],
"group_messages": [
{ "group_id": 1, "group_name": "Project Team", "unseen_count": 4 }
],
"total": 14
}Since there is no JWT authentication yet, authorization uses a requester_id field passed in request bodies. In the future this would be extracted from the auth token.
| Action | Required Role | Endpoint |
|---|---|---|
| Add member | Owner or Admin | POST /groups/:id/members |
| Assign owner/admin role | Owner only | POST /groups/:id/members (with role: "owner" or "admin") |
| Remove member | Owner or Admin | DELETE /groups/:id/members/:user_id |
| Remove the owner | Nobody (forbidden) | DELETE /groups/:id/members/:user_id (target is owner) |
| Update member role | Owner or Admin | PATCH /groups/:id/members/:user_id |
| Change owner's role | Nobody (forbidden) | PATCH /groups/:id/members/:user_id (target is owner) |
| Assign owner role | Owner only | PATCH /groups/:id/members/:user_id (with role: "owner") |
| Update group name | Owner only | PUT /groups/:id |
| Delete group | Owner only | DELETE /groups/:id |
| Leave group | Member (not owner) | POST /groups/:id/leave |
| Send group message | Member | POST /messages (with group_id) |
Auth errors return HTTP 403 Forbidden.
Every layer defines an interface and a private struct implementation:
repository/βUserRepository,GroupRepository,MessageRepository,FileRepositoryservice/βUserService,GroupService,MessageService,FileServicehandler/βUserHandler,GroupHandler,MessageHandler,FileHandler- Constructors:
New*Repository(db),New*Service(repo),New*Handler(service)
Both AddMember (GroupMember) and MarkMessageSeen (Seen) use clause.OnConflict to upsert:
r.db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "group_id"}, {Name: "user_id"}},
DoUpdates: clause.AssignmentColumns([]string{"role", "updated_at"}),
}).Create(&member)Models with gorm.DeletedAt support soft deletes. The DeleteGroup method also cascades to GroupMember records.
Constants in models/message.go: MessageTypeText = "text", MessageTypeImage = "image", MessageTypeFile = "file"
File messages are auto-detected: images (Content-Type starts with "image/") get type "image", everything else gets type "file".
Defined in models/group.go: RoleOwner = "owner", RoleAdmin = "admin", RoleMember = "member"
All conversation queries use limit (default 50) and offset (default 0) query parameters.
- HTTP 400: Bad request (validation errors, invalid conversation target)
- HTTP 403: Forbidden (authorization failures)
- HTTP 404: Resource not found
- HTTP 429: Too many requests (rate limit exceeded)
- HTTP 500: Internal server errors
- All errors returned as
{"error": "<message>"}
main.go uses http.Server with signal handling for graceful shutdown:
- Listens for
SIGINT(Ctrl+C) andSIGTERM(Docker) - 10-second timeout allows in-flight requests to finish
- Database connection is closed after the server shuts down
Per-IP sliding window rate limiter (middleware/rate_limiter.go):
- 100 req/min for
/users,/groups,/messages - 10 req/min for
/upload(file uploads) - No limit for
/healthand static/uploads/* - Returns HTTP 429 with JSON error when exceeded
- Uses
sync.RWMutex+map[string][]time.Timefor tracking - Periodic cleanup goroutine purges stale entries
All JSON endpoints use Gin binding tags for required fields. Additional handler-level validation:
- Message type: Must be one of
text,image, orfile(enum check) - Message content: Required when
type=text - File ID: Required when
type=imageortype=file - Role: Must be one of
owner,admin, ormember(if provided inAddMember) - File size: Max 50MB via
http.MaxBytesReader - Query params: Invalid
receiver_id/group_idreturn 400 (not silently ignored) - Conversation target:
GetConversationandSendFileMessagevalidate at least one target is provided
The package-level DB variable in db/db.go is read after initialization only.
- Model β Add struct in
models/with GORM tags - Repository β Add interface + implementation in
repository/ - Service β Add interface + implementation in
service/ - Handler β Add Gin handler in
handler/ - Middleware β Add any cross-cutting middleware in
middleware/ - Routes β Register endpoint in
routes/routes.go - Migrations β Add model to
db.AutoMigrate()indb/db.go - Tests β Add test files for each layer using established patterns
# Set env vars (or use defaults):
export DB_HOST=localhost DB_PORT=5432 DB_USER=postgres DB_PASSWORD=postgres DB_NAME=chat
go run main.go # Starts on :8080 with graceful shutdowndocker compose up --build # Starts PostgreSQL + appgo test ./... # All tests (uses SQLite in-memory, no DB required)
go test ./... -v # Verbose
go test ./... -cover # Coverage
go test ./middleware/... # Middleware tests only- Connection configured via env vars:
DB_HOST,DB_PORT,DB_USER,DB_PASSWORD,DB_NAME,DB_SSLMODE - Auto-migration runs on every start
- Tests use isolated SQLite in-memory databases (no PostgreSQL needed)
golangci-lint run # Uses .golangci.yml configgo get <package>
go mod tidy| Field | Validation |
|---|---|
| username | Required, unique |
Required, valid email format (Gin binding:"email"), unique |
|
| group name | Required, unique |
| sender_id | Required for messages |
| message type | Required, must be text, image, or file (enum check at handler) |
| message content | Required when type=text |
| file_id | Required when type=image or type=file |
| receiver_id / group_id | Exactly one must be set (service layer) + handler validates at least one |
| requester_id | Required for group management endpoints |
| role (group member) | Must be owner, admin, or member if provided |
| file | Required for /upload and /messages/upload, max 50MB |
Tests use SQLite in-memory (:memory: with MaxOpenConns(1)) β no PostgreSQL needed.
| Package | Test Files | Approach |
|---|---|---|
repository/ |
4 test files, ~55 subtests | Real SQLite in-memory, full CRUD + edge cases |
service/ |
3 test files, ~55 subtests | Mocked repositories via function-field mocks |
handler/ |
5 test files, ~85 subtests | Mocked services via function-field mocks, httptest.NewRecorder() |
middleware/ |
1 test file, 6 subtests | Real Gin engine with test routes, httptest.NewRecorder() |
type mockGroupRepo struct {
addMemberFn func(groupID, userID uint, role string) error
getMemberRoleFn func(groupID, userID uint) (string, error)
// ... each interface method has a function field
}This pattern avoids import-time mocking frameworks and gives fine-grained control per test.
func setupMessageRoutes(svc *mockMsgSvc, fileSvc *mockFileSvc) *gin.Engine {
gin.SetMode(gin.TestMode)
h := NewMessageHandler(svc, fileSvc)
r := gin.New()
r.POST("/messages/upload", h.SendFileMessage)
// ... register all routes
return r
}Tests send HTTP requests through Gin's test router and assert on response code + body.
func setupTestRoute(limiter gin.HandlerFunc) *gin.Engine {
gin.SetMode(gin.TestMode)
r := gin.New()
r.GET("/test", limiter, func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "ok"})
})
return r
}Middleware is tested by wrapping a simple downstream handler and asserting behavior (allows, blocks, resets, etc.).
| Module | Purpose |
|---|---|
github.com/gin-gonic/gin v1.10.0 |
HTTP router + middleware |
gorm.io/gorm v1.25.7 |
ORM |
gorm.io/driver/postgres v1.5.7 |
PostgreSQL driver for GORM (production) |
gorm.io/driver/sqlite v1.5.7 |
SQLite driver for GORM (tests only) |
github.com/stretchr/testify v1.9.0 |
Test assertions (assert, require) + mocks |
- Build:
golang:1.22-alpineβCGO_ENABLED=0+-ldflags="-s -w"β small static binary - Runtime:
alpine:3.19withca-certificatesandtzdata - Exposes port 8080, creates
/app/uploadsdirectory
services:
db: # postgres:16-alpine with healthcheck
app: # builds from Dockerfile, depends on healthy db- Persistent volumes:
pgdata(database),uploads(files) GIN_MODE=releasefor productiondocker stopsends SIGTERM β app handles graceful shutdown
- This is a Go module named
chatβ all imports usechat/...prefix. - Gin context (
*gin.Context) is used for all HTTP handlers β usec.ShouldBindJSONfor JSON,c.PostForm/c.Request.FormFilefor multipart. - Rate limiter is per-IP β
c.ClientIP()is used for tracking. Behind a reverse proxy, you may need to trust proxy headers (r.ForwardedByClientIPorr.SetTrustedProxies). - GORM is used for all database operations β
Preloadfor eager loading,Clauses(clause.OnConflict{})for upsert. - No authentication middleware exists yet β
requester_idis passed in request bodies as a placeholder. - No WebSocket support β this is a REST-only API.
- The
GetUnseenCountDetailedrepository method returns function-local types insidemap[string]interface{}. Tests handle this via JSON round-trip (jsonRoundTriphelper). - When modifying existing code, look at both the interface and the implementation in the same file (repository, service layers).
- Mock files are in the same package as tests (suffixed
_test.go) and use function-field mocks, nottestify/mock. - Tests use SQLite in-memory, not PostgreSQL β
repository/setup_test.goprovides thesetupTestDB()helper. - File uploads go to
./uploads/directory (served statically at/uploads/*), max 50MB. - Graceful shutdown β
main.gouseshttp.Serverwithsignal.Notifyfor SIGINT/SIGTERM. - Middleware is applied per-route-group β define middleware in
routes/routes.goviar.Group("/path", middlewareFn).
| Commit | Description |
|---|---|
2e17b90 |
Phase 1: Service tests, handler tests, lint config, error fixes |
7b07ea1 |
Phase 2: Authorization checks and file upload endpoint |
48d62c5 |
Switch database from SQLite to PostgreSQL |
0ac35b1 |
Add Docker setup with multi-stage build and docker-compose |
cf524fd |
Add combined file upload + message sending endpoint |
ead2ebf |
Add graceful shutdown with signal handling |
a2652a9 |
Add STRUCTURE.md with comprehensive project documentation |
4294b12 |
Add input validation across all handler endpoints |
e011299 |
Add per-IP sliding window rate limiter middleware |
400fe59 |
Add unit tests for rate limiter middleware |
8b27600 |
Update STRUCTURE.md with rate limiter, graceful shutdown, and validation docs |
9034a3f |
Add 5 missing group API endpoints with full test coverage |
7f5af42 |
Update STRUCTURE.md with 5 new group endpoints and updated stats |
5815e7f |
Add private chat features: conversation list and edit message |
26f7c6a |
Update Postman collection with conversations list and edit message endpoints |