Skip to content

feat: add API / JSON endpoint generation (Roadmap 2.4)#279

Merged
adnaan merged 2 commits intomainfrom
api-endpoints
Mar 23, 2026
Merged

feat: add API / JSON endpoint generation (Roadmap 2.4)#279
adnaan merged 2 commits intomainfrom
api-endpoints

Conversation

@adnaan
Copy link
Copy Markdown
Contributor

@adnaan adnaan commented Mar 23, 2026

Summary

  • New lvt gen api <resource> <fields...> command generating pure HTTP REST JSON endpoints
  • New pkg/cors/ package with CORS middleware (functional options, preflight handling)
  • API handler with Go 1.22+ method routing, JSON envelope responses, pagination, proper status codes
  • Paginated SQL queries (LIMIT/OFFSET + COUNT) appended alongside existing CRUD queries
  • Reuses existing DB schema if already generated by lvt gen resource
  • ContextAPI = "api" token constant added for future bearer auth
  • Both multi and single kit templates

Test plan

  • CORS tests: preflight, actual requests, specific origins, credentials, custom headers
  • Golden test: API handler snapshot
  • Content validation: handler has all 5 CRUD methods, routes, JSON helpers, pagination, validator
  • SQL validation: paginated query + COUNT query alongside base CRUD queries
  • Syntax check: generated handler has no syntax errors
  • Full flow: gen app → gen api posts title content:text published:bool → sqlc → go build succeeds
  • All test packages pass (zero regressions)

🤖 Generated with Claude Code

New command: lvt gen api <resource> <fields...>
Generates pure HTTP REST JSON endpoints — no LiveTemplate, no WebSocket.

New package: pkg/cors/
- CORS middleware with functional options (WithOrigins, WithMethods, etc.)
- Preflight OPTIONS handling, origin validation, header configuration

API handler template (api/handler.go.tmpl):
- RESTful CRUD: HandleList, HandleGet, HandleCreate, HandleUpdate, HandleDelete
- Go 1.22+ method-based routing via http.ServeMux
- JSON envelope: {"data": ..., "meta": {"page", "per_page", "total", "total_pages"}}
- Error responses: {"error": {"code": "...", "message": "..."}}
- Proper status codes: 200/201/204/400/404/422
- Input validation with go-playground/validator
- Pagination via ?page=1&per_page=20 query params

API queries template (api/queries.sql.tmpl):
- ListResourcePaginated with LIMIT/OFFSET
- CountResource for pagination metadata

Generator (internal/generator/api.go):
- Reuses existing DB schema if already generated by lvt gen resource
- Generates schema/migration/queries if no existing schema
- Appends paginated queries alongside existing ones
- Injects route at /api/v1/<resource>/ into main.go

CLI (commands/api.go):
- Field parsing with type inference (reuses parseFieldsWithInference)
- Prints endpoint summary and next steps

Both multi and single kit templates included.
ContextAPI = "api" constant added to pkg/token/.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 23, 2026 07:46
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new lvt gen api scaffold that generates REST-style JSON CRUD endpoints (with pagination SQL) plus a reusable CORS middleware package, integrating API generation into both multi/single kits and expanding the test suite to cover generation + runtime compilation.

Changes:

  • Introduces lvt gen api and the internal/generator.GenerateAPI pipeline (handlers, tests, SQL, route injection, resource registration).
  • Adds pkg/cors middleware with option-based configuration and unit tests.
  • Extends templates + golden/integration tests to validate generated API handler output and SQL pagination queries.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
testdata/golden/api_handler.go.golden Golden snapshot for generated API handler output.
pkg/token/token.go Adds ContextAPI token context constant for future bearer auth.
pkg/cors/cors.go New CORS middleware with functional options and preflight handling.
pkg/cors/cors_test.go Unit tests for CORS behavior (preflight, origins, credentials, headers).
internal/kits/system/single/templates/api/test.go.tmpl API test template for single kit.
internal/kits/system/single/templates/api/queries.sql.tmpl Adds paginated list + count queries template for single kit.
internal/kits/system/single/templates/api/handler.go.tmpl API handler template for single kit (CRUD + pagination).
internal/kits/system/multi/templates/api/test.go.tmpl API test template for multi kit.
internal/kits/system/multi/templates/api/queries.sql.tmpl Adds paginated list + count queries template for multi kit.
internal/kits/system/multi/templates/api/handler.go.tmpl API handler template for multi kit (CRUD + pagination).
internal/generator/api.go New generator implementation for API handlers + SQL + main.go route injection.
integration_test.go Adds generation + full-flow integration tests for API generation and compilation.
golden_test.go Adds golden test to compare generated API handler vs snapshot.
commands/gen.go Registers gen api subcommand in dispatcher + help text.
commands/api.go New CLI entrypoint for lvt gen api.

Comment on lines +6 to +11
"fmt"
"net/http"
"net/http/httptest"
"testing"

"[[.ModuleName]]/database/models"
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This generated test template has unused imports (fmt and the database/models import aren’t referenced anywhere). That will cause go test ./... to fail in generated apps. Either remove unused imports or add a compile-safe stub that actually uses them (or move the CRUD test behind a build tag).

Suggested change
"fmt"
"net/http"
"net/http/httptest"
"testing"
"[[.ModuleName]]/database/models"
"net/http"
"net/http/httptest"
"testing"

Copilot uses AI. Check for mistakes.
Comment thread pkg/cors/cors.go
Comment on lines +79 to +88
if len(cfg.AllowOrigins) == 1 && cfg.AllowOrigins[0] == "*" {
w.Header().Set("Access-Control-Allow-Origin", "*")
} else {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Vary", "Origin")
}

if cfg.AllowCredentials {
w.Header().Set("Access-Control-Allow-Credentials", "true")
}
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When AllowCredentials is true and AllowOrigins is {"*"}, the middleware currently responds with Access-Control-Allow-Origin: * plus Access-Control-Allow-Credentials: true. That combination is invalid per the CORS spec and browsers will reject it. If credentials are enabled, echo the request Origin (and set Vary: Origin) instead of *, or reject configuration that combines credentials with wildcard origins.

Copilot uses AI. Check for mistakes.
Comment thread pkg/cors/cors.go
Comment on lines +79 to +84
if len(cfg.AllowOrigins) == 1 && cfg.AllowOrigins[0] == "*" {
w.Header().Set("Access-Control-Allow-Origin", "*")
} else {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Vary", "Origin")
}
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The middleware sets Vary: Origin by overwriting any existing Vary header value. This can break caching behavior when other middleware already added Vary (e.g. Accept-Encoding). Consider appending Origin to Vary (deduplicated) rather than replacing it.

Copilot uses AI. Check for mistakes.
Comment thread internal/generator/api.go
Comment on lines +33 to +43
if kitName == "" {
kitName = "multi"
}

projectConfig, err := config.LoadProjectConfig(basePath)
if err != nil {
return fmt.Errorf("failed to load project config: %w", err)
}
if kitName == "" {
kitName = projectConfig.GetKit()
}
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In GenerateAPI, the kit selection logic is inconsistent: kitName is defaulted to "multi" before loading .lvtrc, so the later if kitName == "" { kitName = projectConfig.GetKit() } branch can never run. Either remove the projectConfig kit lookup, or move the defaulting until after reading .lvtrc so callers can omit kitName and still use the project’s configured kit.

Suggested change
if kitName == "" {
kitName = "multi"
}
projectConfig, err := config.LoadProjectConfig(basePath)
if err != nil {
return fmt.Errorf("failed to load project config: %w", err)
}
if kitName == "" {
kitName = projectConfig.GetKit()
}
projectConfig, err := config.LoadProjectConfig(basePath)
if err != nil {
return fmt.Errorf("failed to load project config: %w", err)
}
if kitName == "" {
kitName = projectConfig.GetKit()
}
if kitName == "" {
kitName = "multi"
}

Copilot uses AI. Check for mistakes.
Comment on lines +6 to +11
"fmt"
"net/http"
"net/http/httptest"
"testing"

"[[.ModuleName]]/database/models"
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This generated test template has unused imports (fmt and the database/models import aren’t referenced anywhere). That will cause go test ./... to fail in generated apps. Either remove unused imports or add a compile-safe stub that actually uses them (or move the CRUD test behind a build tag).

Suggested change
"fmt"
"net/http"
"net/http/httptest"
"testing"
"[[.ModuleName]]/database/models"
"net/http"
"net/http/httptest"
"testing"

Copilot uses AI. Check for mistakes.
Comment thread commands/api.go
Comment on lines +41 to +52
// Parse flags
skipValidation := false
var filteredArgs []string
for i := 0; i < len(args); i++ {
if args[i] == "--skip-validation" {
skipValidation = true
} else {
filteredArgs = append(filteredArgs, args[i])
}
}
_ = skipValidation

Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--skip-validation is parsed but never used (it’s assigned to _), so the flag is currently a no-op despite being documented in help output. This should either run the same runPostGenValidation(...) used by other lvt gen * commands when the flag is not set, or the flag/help text should be removed.

Copilot uses AI. Check for mistakes.
Comment thread commands/api.go
Comment on lines +34 to +40
loader := kits.DefaultLoader()
kitInfo, err := loader.Load(kit)
if err != nil {
return fmt.Errorf("failed to load kit: %w", err)
}
_ = kitInfo

Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kitInfo is loaded and then immediately discarded (_ = kitInfo). This adds I/O and code noise without affecting behavior. Either use kitInfo for something meaningful (e.g., validate kit supports API templates) or remove the load entirely.

Copilot uses AI. Check for mistakes.
Comment thread internal/generator/api.go
Comment on lines +197 to +204
Path: "/api/v1/" + resourceNameLower + "/",
PackageName: "api",
HandlerCall: "api.Handler(queries)",
ImportPath: moduleName + "/app/api",
}
if err := InjectRoute(mainGoPath, route); err != nil {
fmt.Printf("⚠️ Could not auto-inject API route: %v\n", err)
fmt.Printf(" Add manually: http.Handle(\"/api/v1/%s/\", api.Handler(queries))\n", resourceNameLower)
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The auto-injected route path includes a trailing slash (/api/v1/<resource>/), but the generated API mux registers routes without the trailing slash (e.g. GET /api/v1/<resource>). When mounted on /api/v1/<resource>/, requests to /api/v1/<resource>/ won’t match any handler (and /api/v1/<resource> may get redirected). Inject the exact path (/api/v1/<resource>) and/or generate handler routes that accept both variants.

Suggested change
Path: "/api/v1/" + resourceNameLower + "/",
PackageName: "api",
HandlerCall: "api.Handler(queries)",
ImportPath: moduleName + "/app/api",
}
if err := InjectRoute(mainGoPath, route); err != nil {
fmt.Printf("⚠️ Could not auto-inject API route: %v\n", err)
fmt.Printf(" Add manually: http.Handle(\"/api/v1/%s/\", api.Handler(queries))\n", resourceNameLower)
Path: "/api/v1/" + resourceNameLower,
PackageName: "api",
HandlerCall: "api.Handler(queries)",
ImportPath: moduleName + "/app/api",
}
if err := InjectRoute(mainGoPath, route); err != nil {
fmt.Printf("⚠️ Could not auto-inject API route: %v\n", err)
fmt.Printf(" Add manually: http.Handle(\"/api/v1/%s\", api.Handler(queries))\n", resourceNameLower)

Copilot uses AI. Check for mistakes.
return
}

updated, _ := h.Queries.Get[[.ResourceNameSingular]]ByID(r.Context(), id)
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HandleUpdate ignores the error from the final Get...ByID call (updated, _ := ...). If the re-fetch fails (e.g., transient DB error), this will return a 200 with a zero-value payload. Handle the error and return an appropriate 5xx (or at least propagate the error) instead of discarding it.

Suggested change
updated, _ := h.Queries.Get[[.ResourceNameSingular]]ByID(r.Context(), id)
updated, err := h.Queries.Get[[.ResourceNameSingular]]ByID(r.Context(), id)
if err != nil {
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to load updated [[.ResourceNameLower]]")
return
}

Copilot uses AI. Check for mistakes.
return
}

updated, _ := h.Queries.Get[[.ResourceNameSingular]]ByID(r.Context(), id)
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HandleUpdate ignores the error from the final Get...ByID call (updated, _ := ...). If the re-fetch fails (e.g., transient DB error), this will return a 200 with a zero-value payload. Handle the error and return an appropriate 5xx (or at least propagate the error) instead of discarding it.

Suggested change
updated, _ := h.Queries.Get[[.ResourceNameSingular]]ByID(r.Context(), id)
updated, err := h.Queries.Get[[.ResourceNameSingular]]ByID(r.Context(), id)
if err != nil {
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to fetch updated [[.ResourceNameLower]]")
return
}

Copilot uses AI. Check for mistakes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@adnaan adnaan merged commit 5fdec1f into main Mar 23, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants