Files
mal/AGENTS.md

3.2 KiB

mal

personal anime tracking platform. go/htmx rewrite.

stack

  • go standard library (net/http)
  • htmx + templ templates
  • sqlite + sqlc
  • tailwind (dark theme)
  • jikan api (myanimelist)

structure

cmd/server/           main entry
internal/
  auth/               sessions, passwords
  database/           sqlc generated, migrations
  handlers/           http handlers by domain
  middleware/         auth, logging
  jikan/              api client
  templates/          templ components
migrations/           sql files
static/               css, js

go patterns

errors

always handle errors explicitly. wrap with context using fmt.Errorf:

if err != nil {
    return fmt.Errorf("failed to fetch user: %w", err)
}

use errors.Is and errors.As to check wrapped errors.

early returns

reduce nesting. check errors first, return early:

func getUser(id string) (*User, error) {
    if id == "" {
        return nil, ErrInvalidID
    }
    
    user, err := db.FindUser(id)
    if err != nil {
        return nil, err
    }
    
    return user, nil
}

defer for cleanup

always close resources with defer:

resp, err := http.Get(url)
if err != nil {
    return err
}
defer resp.Body.Close()

interfaces

accept interfaces, return structs. keep interfaces small:

type Reader interface {
    Read(p []byte) (n int, err error)
}

naming

  • short, lowercase package names: auth, jikan, db
  • MixedCaps for exports, mixedCaps for internal
  • getters: Owner() not GetOwner()
  • interfaces: single method = method name + er suffix (Reader, Writer)

zero values

design structs so zero value is useful:

var buf bytes.Buffer // ready to use, no init needed
buf.WriteString("hello")

composition over inheritance

embed types to compose behavior:

type Handler struct {
    db     *database.Queries
    jikan  *jikan.Client
    logger *slog.Logger
}

htmx

check for htmx requests:

func isHTMX(r *http.Request) bool {
    return r.Header.Get("HX-Request") == "true"
}

return partials for htmx, full pages otherwise. use hx-swap-oob for multiple updates. trigger toasts with HX-Trigger header.

templ

render components directly to response:

func (h *Handler) Home(w http.ResponseWriter, r *http.Request) {
    templates.HomePage(data).Render(r.Context(), w)
}

pass data explicitly. keep components focused. use layouts.

database

sqlc generates type-safe queries. always use parameterized queries.

tables: user, session, account, anime, watch_list_entry

watch statuses: watching, completed, on_hold, dropped, plan_to_watch

jikan api

rate limit: 3 req/sec max. stagger batch requests. cache in local db.

commands

make dev          # hot reload (air)
make build        # binary
make test         # tests
make migrate      # run migrations
make sqlc         # generate code
make create-user  # cli user creation

env

DATABASE_FILE=mal.db
SESSION_SECRET=min_32_chars_random
PORT=3000

avoid

  • panics in handlers
  • forgetting defer resp.Body.Close()
  • unstaggered jikan requests (429 errors)
  • globals for config/state
  • large monolithic templates