# Novu Spectral Ruleset # Opinionated rules enforcing the patterns observed in the Novu OpenAPI specification. # Covers: REST surface conventions, naming, security (secretKey ApiKey header), rate-limit headers, # pagination, response shapes, and operation metadata. extends: - spectral:oas functions: [] rules: # ── INFO / METADATA ───────────────────────────────────────────────────── info-title-novu-prefix: description: API title should identify the API as a Novu surface. message: "info.title should start with 'Novu' (got: '{{value}}')." severity: warn given: $.info.title then: function: pattern functionOptions: match: "^Novu" info-description-required: description: info.description must be present and non-empty. severity: warn given: $.info then: field: description function: truthy info-contact-required: description: info.contact should be defined. severity: info given: $.info then: field: contact function: truthy info-license-required: description: info.license should be defined (Novu is MIT licensed). severity: info given: $.info then: field: license function: truthy info-version-semver: description: info.version should follow semantic versioning. severity: warn given: $.info.version then: function: pattern functionOptions: match: "^\\d+\\.\\d+\\.\\d+(-[A-Za-z0-9.-]+)?$" # ── OPENAPI VERSION ───────────────────────────────────────────────────── openapi-version-3: description: Use OpenAPI 3.x. severity: error given: $.openapi then: function: pattern functionOptions: match: "^3\\." # ── SERVERS ───────────────────────────────────────────────────────────── servers-https-only: description: All servers MUST use HTTPS. severity: error given: $.servers[*].url then: function: pattern functionOptions: match: "^https://" servers-novu-domain: description: Server URLs should target a novu.co domain. severity: info given: $.servers[*].url then: function: pattern functionOptions: match: "novu\\.co" # ── PATHS — NAMING ────────────────────────────────────────────────────── paths-kebab-case: description: Path segments (excluding parameters) should use kebab-case. severity: warn given: $.paths.*~ then: function: pattern functionOptions: match: "^(/(v[0-9]+|[a-z0-9][a-z0-9-]*|\\{[^/]+\\}))+$" paths-version-prefix: description: Every path should be versioned (e.g. /v1/, /v2/). severity: warn given: $.paths.*~ then: function: pattern functionOptions: match: "^/v[0-9]+/" paths-no-trailing-slash: description: Paths must not end with a trailing slash. severity: error given: $.paths.*~ then: function: pattern functionOptions: notMatch: ".+/$" paths-no-query-string: description: Path keys must not include query strings. severity: error given: $.paths.*~ then: function: pattern functionOptions: notMatch: "\\?" # ── OPERATIONS ────────────────────────────────────────────────────────── operation-operationid-required: description: Every operation must declare an operationId. severity: error given: $.paths.*[get,post,put,patch,delete] then: field: operationId function: truthy operation-operationid-camelcase: description: operationId should be camelCase (Novu uses ControllerName_methodName pattern). severity: warn given: $.paths.*[get,post,put,patch,delete].operationId then: function: pattern functionOptions: match: "^[A-Za-z][A-Za-z0-9_]*$" operation-summary-required: description: Every operation must declare a summary. severity: error given: $.paths.*[get,post,put,patch,delete] then: field: summary function: truthy operation-summary-novu-prefix: description: Operation summary should start with the 'Novu' provider prefix and be in Title Case. severity: warn given: $.paths.*[get,post,put,patch,delete].summary then: function: pattern functionOptions: match: "^Novu " operation-description-required: description: Every operation should declare a description. severity: warn given: $.paths.*[get,post,put,patch,delete] then: field: description function: truthy operation-tags-required: description: Every operation should declare at least one tag. severity: warn given: $.paths.*[get,post,put,patch,delete] then: field: tags function: truthy # ── TAGS ──────────────────────────────────────────────────────────────── tags-title-case: description: Tag names should be in Title Case (matches the resource grouping in the Novu Dashboard). severity: warn given: $.tags[*].name then: function: pattern functionOptions: match: "^[A-Z][A-Za-z0-9]*( [A-Z][A-Za-z0-9]*)*$" # ── PARAMETERS ────────────────────────────────────────────────────────── parameter-description-required: description: Every parameter should declare a description. severity: warn given: $.paths.*[get,post,put,patch,delete].parameters[*] then: field: description function: truthy parameter-camelcase: description: Parameter names should be camelCase (Novu convention, e.g. subscriberId, environmentId). severity: warn given: $.paths.*[get,post,put,patch,delete].parameters[?(@.in!='header')].name then: function: pattern functionOptions: match: "^[a-z][A-Za-z0-9]*$" parameter-pagination-cursor-limit: description: Paginated list endpoints should expose cursor + limit parameters. severity: info given: $.paths.*.get.parameters[?(@.name=='limit')] then: field: schema function: truthy # ── REQUEST BODIES ────────────────────────────────────────────────────── request-body-application-json: description: Request bodies should accept application/json. severity: warn given: $.paths.*[post,put,patch].requestBody.content then: field: "application/json" function: truthy # ── RESPONSES ─────────────────────────────────────────────────────────── response-2xx-required: description: Every operation must define at least one 2xx response. severity: error given: $.paths.*[get,post,put,patch,delete].responses then: function: schema functionOptions: schema: anyOf: - required: ["200"] - required: ["201"] - required: ["202"] - required: ["204"] response-401-required: description: Authenticated operations should document the 401 Unauthorized response. severity: warn given: $.paths.*[get,post,put,patch,delete].responses then: field: "401" function: truthy response-429-required: description: Rate-limited operations should document the 429 Too Many Requests response. severity: info given: $.paths.*[get,post,put,patch,delete].responses then: field: "429" function: truthy response-error-content-type: description: Error responses should return application/json. severity: warn given: $.paths.*[get,post,put,patch,delete].responses[?(@property.match(/^[45]/))].content then: field: "application/json" function: truthy response-rate-limit-headers: description: Successful responses should expose RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset / RateLimit-Policy headers per Novu's policy. severity: info given: $.paths.*[get,post,put,patch,delete].responses[?(@property.match(/^2/))].headers then: field: "RateLimit-Limit" function: truthy response-idempotency-headers: description: Mutating operations should expose Idempotency-Key / Idempotency-Replay response headers. severity: info given: $.paths.*[post,put,patch,delete].responses[?(@property.match(/^2/))].headers then: field: "Idempotency-Key" function: truthy # ── SCHEMAS ───────────────────────────────────────────────────────────── schema-property-camelcase: description: Schema property names should be camelCase (Novu DTO convention). severity: warn given: $.components.schemas[*].properties.*~ then: function: pattern functionOptions: match: "^[a-z][A-Za-z0-9]*$" schema-type-required: description: Top-level schemas must declare a type. severity: warn given: $.components.schemas[*] then: function: schema functionOptions: schema: anyOf: - required: ["type"] - required: ["$ref"] - required: ["oneOf"] - required: ["anyOf"] - required: ["allOf"] schema-dto-suffix: description: Request/response DTO schemas should use the Dto suffix Novu adopts (RequestDto / ResponseDto / Dto). severity: info given: $.components.schemas then: function: schema functionOptions: schema: patternProperties: "Dto$": {} # ── SECURITY ──────────────────────────────────────────────────────────── security-scheme-secretkey-required: description: components.securitySchemes must define the secretKey ApiKey scheme used by Novu. severity: error given: $.components.securitySchemes then: field: secretKey function: truthy security-apikey-header: description: The secretKey scheme should be an ApiKey scheme delivered in the Authorization header. severity: warn given: $.components.securitySchemes.secretKey then: function: schema functionOptions: schema: type: object properties: type: { const: apiKey } in: { const: header } name: { const: Authorization } # ── HTTP METHOD CONVENTIONS ───────────────────────────────────────────── get-no-request-body: description: GET operations should not declare a request body. severity: error given: $.paths.*.get.requestBody then: function: falsy delete-204-or-200: description: DELETE operations should respond with 200 or 204. severity: info given: $.paths.*.delete.responses then: function: schema functionOptions: schema: anyOf: - required: ["200"] - required: ["202"] - required: ["204"] # ── GENERAL QUALITY ───────────────────────────────────────────────────── components-schemas-defined: description: components.schemas should be defined. severity: warn given: $.components then: field: schemas function: truthy no-empty-description: description: Descriptions, when present, must be non-empty. severity: warn given: $..description then: function: truthy