Skip to main content

api

Add or modify API endpoints using TypeSpec. Use when adding new API routes, modifying request/response types, or changing the OpenAPI spec.

설치로 이동

소스 정보

저장소
openmeterio/openmeter
최근 소스 활동
2026년 8월 31일 14:43
감지된 SKILL.md 언어
영어
스타
2,327
포크
220

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

파일 탐색기
20 개 파일

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
api
description
Add or modify API endpoints using TypeSpec. Use when adding new API routes, modifying request/response types, or changing the OpenAPI spec.
allowed-tools
Read, Edit, Write, Bash, Grep, Glob, Agent
# API Development You are helping the user add or modify API endpoints in OpenMeter. ## Context - **API spec source:** `api/spec/packages/` — TypeSpec definitions (two packages: `aip` for v3, `legacy` for v1) - **Local server port:** The server runs on port 8888 by default locally (`http://localhost:8888/api/v3`) - **New APIs go in AIP package:** `api/spec/packages/aip/src/` — all new endpoints must be added here - **Generated outputs (DO NOT edit manually):** - `api/openapi.yaml`, `api/openapi.cloud.yaml` — OpenAPI specs - `api/client/javascript/`, `api/client/go/` — SDK clients - `api/api.gen.go`, `api/v3/api.gen.go` — Go server code (oapi-codegen) ## AIP (v3) API Structure ```text api/spec/packages/aip/src/ ├── main.tsp # Top-level imports ├── openmeter.tsp # Service definition, routes, and interface wiring ├── konnect.tsp # Konnect-specific service definition (must mirror openmeter.tsp's tags + route interfaces) ├── common/ # Shared types: errors, pagination, parameters ├── shared/ # Shared resources: ULID, request/response wrappers, tags ├── meters/ # Domain: models + operations ├── customers/ # Domain: models + operations ├── subscriptions/ # ... ├── billing/ ├── apps/ ├── currencies/ ├── llmcost/ └── ... ``` Each domain typically has: - `index.tsp` — imports for the domain - `<resource>.tsp` — model/type definitions - `operations.tsp` — interface with CRUD operations Routes are wired in `api/spec/packages/aip/src/openmeter.tsp` via interface declarations with `@route` and `@tag` decorators. `api/spec/packages/aip/src/konnect.tsp` is the parallel Konnect-flavoured service definition. The two files are **not** identical — Konnect has its own service metadata, namespace name, `@useAuth` configuration, security scheme models, and intentionally exposes a narrower subset of the OpenMeter surface. But for any domain that *is* exposed in both, every new domain `import`, `@tagMetadata(...)` entry, and `@route` / `@tag` interface must be added to both files in the same edit. Diff the two files before generating to spot accidental drift; existing differences are expected, but a tag/route you just added showing up in only one file is a bug. ## Workflow Follow these steps in order: ### Step 1: Edit the TypeSpec API spec For a new domain/resource: 1. Create a new directory under `api/spec/packages/aip/src/<domain>/` 2. Add `index.tsp`, model file(s), and `operations.tsp` 3. Import the domain in `api/spec/packages/aip/src/openmeter.tsp` **and** `api/spec/packages/aip/src/konnect.tsp` (unless the domain is intentionally OpenMeter-only — confirm with the user before excluding it from Konnect) 4. Wire up the route interface (and any new `@tagMetadata`) in **both** `openmeter.tsp` and `konnect.tsp`. After editing, run `diff openmeter.tsp konnect.tsp` and check that your new imports / tags / interfaces appear on both sides — pre-existing differences (service metadata, namespace name, `@useAuth`, security scheme models) are intentional and unrelated to your change. For modifying an existing endpoint: 1. Find the relevant files under `api/spec/packages/aip/src/<domain>/` 2. Edit the model or operations as needed Look at existing domains (e.g., `meters/`, `customers/`) for conventions: - Use `Shared.CreateRequest<T>`, `Shared.GetResponse<T>`, `Shared.PagePaginatedResponse<T>` wrappers - For v3 cursor-based list endpoints, prefer `Shared.CursorPaginatedResponse<T>` over endpoint-specific cursor meta models. In generated Go, this maps to `api.CursorMetaPage`, where `next` / `previous` are `nullable.Nullable[string]` and `size` is `float32`; handlers may still return opaque cursor tokens and leave `first` / `last` unset. - Use `Common.ErrorResponses`, `Common.NotFound` for error types - Use `Common.PagePaginationQuery` for list operations - Use `@operationId`, `@summary`, `@tag` decorators on operations - Use `Shared.ULID` for resource IDs in path parameters - Routes follow the pattern `/openmeter/<resource>` ### Step 2: Generate API code Run: ```bash make gen-api ``` This generates the OpenAPI spec, SDK clients, and Go server stubs. Check that it completes without errors. Then run: ```bash make generate ``` This regenerates Go server code from the updated OpenAPI spec (oapi-codegen). ### Step 3: Implement the handler After generating, implement the handler package and wire it into the server. #### Handler Package Structure Each handler domain lives at `api/v3/handlers/<domain>/` and contains: - `handler.go` — Handler interface + constructor - `<operation>.go` — One file per operation (create.go, list.go, get.go, delete.go) - `convert.go` — Domain ↔ API type mapping functions Reference: `api/v3/handlers/llmcost/` #### Handler Interface & Constructor (`handler.go`) ```go package <domain> type Handler interface { List<Resource>s() List<Resource>sHandler Create<Resource>() Create<Resource>Handler Get<Resource>() Get<Resource>Handler Delete<Resource>() Delete<Resource>Handler } type handler struct { resolveNamespace func(ctx context.Context) (string, error) service <domain>.Service options []httptransport.HandlerOption } func New( resolveNamespace func(ctx context.Context) (string, error), service <domain>.Service, options ...httptransport.HandlerOption, ) Handler { return &handler{ resolveNamespace: resolveNamespace, service: service, options: options, } } ``` Reference: `api/v3/handlers/llmcost/handler.go` #### Handler Operation Pattern (`<operation>.go`) Each operation file uses `httptransport.NewHandlerWithArgs` with 4 arguments: 1. **Request decoder** — parse HTTP request → domain input, resolve namespace 2. **Operation function** — call service, map result to API response type 3. **Response encoder** — `commonhttp.JSONResponseEncoderWithStatus[T](http.StatusXxx)` 4. **Options** — `httptransport.AppendOptions(h.options, httptransport.WithOperationName("..."), httptransport.WithErrorEncoder(apierrors.GenericErrorEncoder()))` > **List endpoints with filtering:** if the operation supports `?filter[...]` query parameters, use the `/api-filters` skill for the decoder and adapter wiring. It covers `api/v3/filters.Parse`, the typed filter structs, `Convert*` helpers, range splitting, and the Ent `.Select(field)` application — everything this skill does not cover. Type alias convention at top of file: ```go type ( List<Resource>sRequest = <domain>.List<Resource>sInput List<Resource>sResponse = response.PagePaginationResponse[api.<Resource>] List<Resource>sParams = api.List<Resource>sParams List<Resource>sHandler = httptransport.HandlerWithArgs[List<Resource>sRequest, List<Resource>sResponse, List<Resource>sParams] ) ``` Full example: ```go func (h *handler) List<Resource>s() List<Resource>sHandler { return httptransport.NewHandlerWithArgs( // 1. Request decoder func(ctx context.Context, r *http.Request, params List<Resource>sParams) (List<Resource>sRequest, error) { ns, err := h.resolveNamespace(ctx) if err != nil { return List<Resource>sRequest{}, err } req := List<Resource>sRequest{ Namespace: ns, } // Pagination req.Page = pagination.NewPage(1, 20) if params.Page != nil { req.Page = pagination.NewPage( lo.FromPtrOr(params.Page.Number, 1), lo.FromPtrOr(params.Page.Size, 20), ) if err := req.Page.Validate(); err != nil { return req, apierrors.NewBadRequestError(ctx, err, apierrors.InvalidParameters{ {Field: "page", Reason: err.Error(), Source: apierrors.InvalidParamSourceQuery}, }) } } // Sort if params.Sort != nil { sort, err := request.ParseSortBy(*params.Sort) if err != nil { return req, apierrors.NewBadRequestError(ctx, err, apierrors.InvalidParameters{ {Field: "sort", Reason: err.Error(), Source: apierrors.InvalidParamSourceQuery}, }) } if !validSortField(sort.Field) { return req, apierrors.NewBadRequestError(ctx, fmt.Errorf("unsupported sort field: %s", sort.Field), apierrors.InvalidParameters{ {Field: "sort", Reason: fmt.Sprintf("unsupported sort field %q", sort.Field), Source: apierrors.InvalidParamSourceQuery}, }) } req.OrderBy = sort.Field req.Order = sort.Order.ToSortxOrder() } return req, nil }, // 2. Operation function func(ctx context.Context, request List<Resource>sRequest) (List<Resource>sResponse, error) { result, err := h.service.List<Resource>s(ctx, request) if err != nil { return List<Resource>sResponse{}, fmt.Errorf("failed to list: %w", err) } items := lo.Map(result.Items, func(item <domain>.<Resource>, _ int) api.<Resource> { return domainToAPI(item) }) return response.NewPagePaginationResponse(items, response.PageMetaPage{ Size: request.Page.PageSize, Number: request.Page.PageNumber, Total: lo.ToPtr(result.TotalCount), }), nil }, // 3. Response encoder commonhttp.JSONResponseEncoderWithStatus[List<Resource>sResponse](http.StatusOK), // 4. Options httptransport.AppendOptions( h.options, httptransport.WithOperationName("list-<resource>s"), httptransport.WithErrorEncoder(apierrors.GenericErrorEncoder()), )..., ) } ``` For handlers without params (e.g., Create), use `httptransport.NewHandler` (3 arguments, no params): ```go type ( Create<Resource>Handler = httptransport.Handler[Create<Resource>Request, Create<Resource>Response] ) ``` Reference: `api/v3/handlers/llmcost/list_prices.go` #### Converters (`convert.go`) Converters translate API request bodies into service/domain types faithfully — they must not enforce domain rules (valid type combinations, required-field semantics, cross-field constraints). Map invalid combinations onto the domain type as-is (zero values where needed) and let the domain `Validate()` report the issue; that path produces user-serviceable errors with codes, field paths, and severity, and keeps draft-with-issues semantics working (warning-severity issues are surfaced on the draft and block publish instead of failing create). A converter may only error when the body cannot be mapped at all — unknown union discriminator, unparsable amount or duration. Wrap such errors in `models.NewGenericValidationError` so they surface as 400, not 500. #### Error Mapping Domain errors auto-map to HTTP status codes via the error encoder: - `GenericNotFoundError` → 404 - `GenericValidationError` → 400 - `GenericConflictError` → 409 - `GenericForbiddenError` → 403 - `GenericPreConditionFailedError` → 412 No need to manually handle these — just return them from the service and the error encoder handles it. #### Structured Validation Errors (ValidationIssue) In v3 API handlers, use `models.ValidationIssue` for structured validation errors with codes, field paths, and severity levels. This is the **handler-layer** pattern — service/adapter layers continue using `models.NewGenericValidationError()`. ```go // Define validation issues as package-level variables var errMissingName = models.NewValidationError("missing_name", "name is required") var errInvalidCurrency = models.NewValidationWarning("invalid_currency", "currency not recognized") // Use with field paths err := errMissingName.WithPathString("body", "name") // Convert from domain errors to structured issues issues, err := models.AsValidationIssues(domainErr) ``` Key types from `pkg/models/validationissue.go`: - `models.NewValidationError(code, message)` — critical severity - `models.NewValidationWarning(code, message)` — warning severity - `models.NewValidationIssue(code, message, opts...)` — with options - `.WithPathString("body", "field")` — attach JSONPath field location - `.WithComponent(component)` — attach component name - `models.AsValidationIssues(err)` — convert error tree to structured issues ### Step 4: Wire Handler into Server Three files to modify: **1. `api/v3/server/server.go`:** - Add service to `Config` struct - Add handler field to `Server` struct - Instantiate handler in `NewServer()` using `<domain>handler.New(resolveNamespace, config.<Domain>Service, httptransport.WithErrorHandler(config.ErrorHandler))` **2. `api/v3/server/routes.go`:** - Add route methods that delegate to handler: ```go // For operations WITH params (list, get by ID, delete by ID): func (s *Server) List<Resource>s(w http.ResponseWriter, r *http.Request, params api.List<Resource>sParams) { s.<domain>Handler.List<Resource>s().With(params).ServeHTTP(w, r) } func (s *Server) Get<Resource>(w http.ResponseWriter, r *http.Request, id api.ULID) { s.<domain>Handler.Get<Resource>().With(id).ServeHTTP(w, r) } // For operations WITHOUT params (create): func (s *Server) Create<Resource>(w http.ResponseWriter, r *http.Request) { s.<domain>Handler.Create<Resource>().ServeHTTP(w, r) } ``` **3. Import the handler package in `server.go`.** Reference: `api/v3/server/server.go:138-218`, `api/v3/server/routes.go` ### Step 5: Review - Check the generated `api/openapi.yaml` or `api/v3/api.gen.go` to verify the endpoints look correct - Present a summary of the API changes to the user ## AIP Standards (Kong AIP) OpenMeter v3 APIs follow [Kong's AIP](https://kong-aip.netlify.app/list/) conventions. Each rule lives in its own file under `rules/` next to this SKILL — open the rule file you need for the task at hand. ### Rule index | File | Covers |
GitHub에서 보기
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기