Skip to content

Commit 78a38f8

Browse files
committed
init commit
0 parents  commit 78a38f8

27 files changed

Lines changed: 4013 additions & 0 deletions

.agent/workflows/agents.md

Lines changed: 332 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,332 @@
1+
---
2+
description: Best practices workflow for building the CAPY Go REST API with type-safe, production-ready code
3+
---
4+
5+
# CAPY API Development Workflow
6+
7+
This workflow ensures type-safe, reliable, and production-ready Go code for the CAPY club management API.
8+
9+
## Prerequisites
10+
11+
Ensure you have the required tools installed:
12+
```bash
13+
go version # Go 1.22+
14+
sqlc version # sqlc CLI
15+
migrate -version # golang-migrate CLI
16+
docker --version # Docker for local dev
17+
```
18+
19+
---
20+
21+
## 1. Database Changes Workflow
22+
23+
When modifying the database schema:
24+
25+
### Step 1.1: Create a New Migration
26+
27+
```bash
28+
migrate create -ext sql -dir migrations -seq <migration_name>
29+
```
30+
31+
### Step 1.2: Write Up Migration
32+
33+
Edit `migrations/XXXXXX_<migration_name>.up.sql` with your DDL statements:
34+
- Use `CREATE TABLE IF NOT EXISTS` for idempotency
35+
- Add proper constraints (NOT NULL, UNIQUE, FOREIGN KEY)
36+
- Include indexes for frequently queried columns
37+
38+
### Step 1.3: Write Down Migration
39+
40+
Edit `migrations/XXXXXX_<migration_name>.down.sql` with rollback statements:
41+
- `DROP TABLE IF EXISTS` in reverse dependency order
42+
- `DROP INDEX IF EXISTS` for any created indexes
43+
44+
### Step 1.4: Apply Migration
45+
46+
```bash
47+
# Ensure DB is running
48+
docker-compose up -d db
49+
50+
# Apply migrations
51+
migrate -path migrations -database "postgres://capy:secret@localhost:5432/capy_db?sslmode=disable" up
52+
```
53+
54+
// turbo-all
55+
56+
---
57+
58+
## 2. sqlc Query Workflow
59+
60+
When adding new database operations:
61+
62+
### Step 2.1: Add Query to queries.sql
63+
64+
Edit `internal/database/queries.sql`. Follow these naming conventions:
65+
66+
```sql
67+
-- CRUD Operations:
68+
-- name: Get<Entity> :one
69+
-- name: List<Entity>s :many
70+
-- name: Create<Entity> :one
71+
-- name: Update<Entity> :one
72+
-- name: Delete<Entity> :exec
73+
74+
-- Example:
75+
-- name: GetUserByEmail :one
76+
SELECT * FROM users WHERE personal_email = $1 OR school_email = $1;
77+
78+
-- name: ListUsersByRole :many
79+
SELECT * FROM users WHERE role = $1 ORDER BY last_name LIMIT $2 OFFSET $3;
80+
```
81+
82+
### Step 2.2: Generate Go Code
83+
84+
```bash
85+
sqlc generate
86+
```
87+
88+
### Step 2.3: Verify Generated Code
89+
90+
Check `internal/database/queries.sql.go` for:
91+
- Correct function signatures
92+
- Proper parameter types (especially UUIDs and custom enums)
93+
- Expected return types
94+
95+
// turbo
96+
97+
---
98+
99+
## 3. Handler Implementation Workflow
100+
101+
When creating a new API endpoint:
102+
103+
### Step 3.1: Define Request/Response Types
104+
105+
Create or update DTOs in the handler file:
106+
107+
```go
108+
type CreateUserRequest struct {
109+
FirstName string `json:"first_name" validate:"required,min=1,max=100"`
110+
LastName string `json:"last_name" validate:"required,min=1,max=100"`
111+
PersonalEmail string `json:"personal_email" validate:"omitempty,email"`
112+
SchoolEmail string `json:"school_email" validate:"omitempty,email"`
113+
Phone string `json:"phone" validate:"omitempty,e164"`
114+
GradYear int `json:"grad_year" validate:"omitempty,gte=2000,lte=2100"`
115+
}
116+
117+
type UserResponse struct {
118+
UID uuid.UUID `json:"uid"`
119+
FirstName string `json:"first_name"`
120+
LastName string `json:"last_name"`
121+
// ... other fields
122+
}
123+
```
124+
125+
### Step 3.2: Implement Handler Function
126+
127+
Follow this pattern:
128+
129+
```go
130+
func (h *Handler) CreateUser(w http.ResponseWriter, r *http.Request) {
131+
// 1. Parse request body
132+
var req CreateUserRequest
133+
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
134+
h.respondError(w, http.StatusBadRequest, "Invalid request body")
135+
return
136+
}
137+
138+
// 2. Validate input
139+
if err := h.validator.Struct(req); err != nil {
140+
h.respondError(w, http.StatusBadRequest, err.Error())
141+
return
142+
}
143+
144+
// 3. Call database via sqlc
145+
user, err := h.queries.CreateUser(r.Context(), database.CreateUserParams{
146+
FirstName: req.FirstName,
147+
LastName: req.LastName,
148+
PersonalEmail: toNullString(req.PersonalEmail),
149+
SchoolEmail: toNullString(req.SchoolEmail),
150+
Phone: toNullString(req.Phone),
151+
GradYear: toNullInt32(req.GradYear),
152+
})
153+
if err != nil {
154+
h.handleDBError(w, err)
155+
return
156+
}
157+
158+
// 4. Return response
159+
h.respondJSON(w, http.StatusCreated, toUserResponse(user))
160+
}
161+
```
162+
163+
### Step 3.3: Register Route
164+
165+
Add route to `internal/router/router.go`:
166+
167+
```go
168+
r.Route("/users", func(r chi.Router) {
169+
r.Use(middleware.Authenticator)
170+
r.Get("/", h.ListUsers)
171+
r.Post("/", h.CreateUser)
172+
r.Route("/{uid}", func(r chi.Router) {
173+
r.Get("/", h.GetUser)
174+
r.Put("/", h.UpdateUser)
175+
r.Delete("/", h.DeleteUser)
176+
})
177+
})
178+
```
179+
180+
---
181+
182+
## 4. Testing Workflow
183+
184+
### Step 4.1: Unit Tests
185+
186+
Create `*_test.go` files alongside handlers:
187+
188+
```go
189+
func TestCreateUser_Success(t *testing.T) {
190+
// Setup mock queries
191+
mockQueries := &database.MockQueries{}
192+
h := NewHandler(mockQueries)
193+
194+
// Create request
195+
body := `{"first_name": "John", "last_name": "Doe"}`
196+
req := httptest.NewRequest("POST", "/users", strings.NewReader(body))
197+
rec := httptest.NewRecorder()
198+
199+
// Execute
200+
h.CreateUser(rec, req)
201+
202+
// Assert
203+
assert.Equal(t, http.StatusCreated, rec.Code)
204+
}
205+
```
206+
207+
### Step 4.2: Integration Tests
208+
209+
```go
210+
//go:build integration
211+
212+
func TestUserAPI_Integration(t *testing.T) {
213+
// Setup real DB connection
214+
pool := setupTestDB(t)
215+
defer pool.Close()
216+
217+
// Run migration
218+
runMigrations(t, pool)
219+
220+
// Test CRUD operations
221+
// ...
222+
}
223+
```
224+
225+
### Step 4.3: Run Tests
226+
227+
```bash
228+
# Unit tests only
229+
go test -v -short ./...
230+
231+
# All tests including integration
232+
docker-compose up -d db
233+
go test -v -tags=integration ./...
234+
```
235+
236+
// turbo
237+
238+
---
239+
240+
## 5. Security Checklist
241+
242+
Before deploying any endpoint:
243+
244+
- [ ] **Authentication**: Endpoint uses `middleware.Authenticator`
245+
- [ ] **Authorization**: Role check matches API design (faculty, org_admin, etc.)
246+
- [ ] **Input Validation**: All user input validated with struct tags
247+
- [ ] **SQL Injection**: Using sqlc (parameterized queries) — automatic ✓
248+
- [ ] **Error Handling**: No sensitive info leaked in error messages
249+
- [ ] **Logging**: Sensitive data (passwords, tokens) never logged
250+
- [ ] **Rate Limiting**: Consider adding for public endpoints
251+
252+
---
253+
254+
## 6. Code Quality Standards
255+
256+
### Type Safety Rules
257+
258+
1. **Always use `uuid.UUID`** — never raw strings for IDs
259+
2. **Use sqlc-generated types** — don't create duplicate structs
260+
3. **Handle nullable fields** — use `pgtype.Text`, `pgtype.Int4`, etc.
261+
4. **Validate at boundaries** — check all input in handlers
262+
263+
### Error Handling Pattern
264+
265+
```go
266+
// Define domain errors
267+
var (
268+
ErrUserNotFound = errors.New("user not found")
269+
ErrDuplicateEmail = errors.New("email already exists")
270+
)
271+
272+
// Wrap database errors
273+
func (h *Handler) handleDBError(w http.ResponseWriter, err error) {
274+
if errors.Is(err, pgx.ErrNoRows) {
275+
h.respondError(w, http.StatusNotFound, "Resource not found")
276+
return
277+
}
278+
279+
var pgErr *pgconn.PgError
280+
if errors.As(err, &pgErr) && pgErr.Code == "23505" {
281+
h.respondError(w, http.StatusConflict, "Duplicate entry")
282+
return
283+
}
284+
285+
// Log unexpected errors
286+
slog.Error("database error", "error", err)
287+
h.respondError(w, http.StatusInternalServerError, "Internal server error")
288+
}
289+
```
290+
291+
---
292+
293+
## 7. Build & Deploy
294+
295+
### Local Development
296+
297+
```bash
298+
# Start all services
299+
docker-compose up --build
300+
301+
# Watch mode (with air)
302+
air
303+
```
304+
305+
### Production Build
306+
307+
```bash
308+
# Build optimized binary
309+
CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o capy-server ./cmd/server
310+
311+
# Build Docker image
312+
docker build -t capy-api:latest .
313+
314+
# Push to registry
315+
docker tag capy-api:latest registry.example.com/capy-api:v1.0.0
316+
docker push registry.example.com/capy-api:v1.0.0
317+
```
318+
319+
---
320+
321+
## Quick Reference Commands
322+
323+
| Task | Command |
324+
|------|---------|
325+
| Generate sqlc | `sqlc generate` |
326+
| New migration | `migrate create -ext sql -dir migrations -seq name` |
327+
| Apply migrations | `migrate -path migrations -database $DATABASE_URL up` |
328+
| Rollback 1 step | `migrate -path migrations -database $DATABASE_URL down 1` |
329+
| Run tests | `go test -v ./...` |
330+
| Build binary | `go build -o capy-server ./cmd/server` |
331+
| Start dev | `docker-compose up --build` |
332+
| View logs | `docker-compose logs -f api` |

.env.example

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# =============================================================================
2+
# SERVER
3+
# =============================================================================
4+
SERVER_HOST=0.0.0.0
5+
SERVER_PORT=8080
6+
7+
# =============================================================================
8+
# DATABASE
9+
# =============================================================================
10+
# In Dokploy: Copy the connection string from your PostgreSQL service
11+
# Locally: Use docker-compose db service
12+
DATABASE_URL=postgres://capy:devpassword@localhost:5432/capy_db?sslmode=disable
13+
14+
# =============================================================================
15+
# AUTHENTICATION
16+
# =============================================================================
17+
# JWT signing key (generate with: openssl rand -base64 32)
18+
JWT_SECRET=REPLACE_ME_WITH_SECURE_SECRET
19+
JWT_EXPIRY_HOURS=24
20+
21+
# Cookie settings
22+
COOKIE_DOMAIN=localhost
23+
COOKIE_SECURE=false
24+
25+
# Google OAuth
26+
GOOGLE_CLIENT_ID=
27+
GOOGLE_CLIENT_SECRET=
28+
GOOGLE_REDIRECT_URL=http://localhost:8080/v1/auth/google/callback
29+
30+
# Microsoft OAuth
31+
MICROSOFT_CLIENT_ID=
32+
MICROSOFT_CLIENT_SECRET=
33+
MICROSOFT_TENANT_ID=common
34+
MICROSOFT_REDIRECT_URL=http://localhost:8080/v1/auth/microsoft/callback
35+
36+
# =============================================================================
37+
# ENVIRONMENT
38+
# =============================================================================
39+
ENV=development

.gitignore

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Build output
2+
bin/
3+
*.exe
4+
5+
# Environment
6+
.env
7+
.env.local
8+
9+
# IDE
10+
.idea/
11+
.vscode/
12+
*.swp
13+
*.swo
14+
15+
# OS
16+
.DS_Store
17+
Thumbs.db
18+
19+
# Temp
20+
.tmp/
21+
22+
# Test coverage
23+
coverage.out
24+
coverage.html

0 commit comments

Comments
 (0)