Novu · API Governance Rules

Novu API Rules

Spectral linting rules defining API design standards and conventions for Novu.

48 Rules error 9 warn 28 info 11
View Rules File View on GitHub

Rule Categories

components delete error get info no openapi operation parameter path paths query request response schema security servers tags

Rules

warn
info-title-novu-prefix
API title should identify the API as a Novu surface.
$.info.title
warn
info-description-required
info.description must be present and non-empty.
$.info
info
info-contact-required
info.contact should be defined.
$.info
info
info-license-required
info.license should be defined (Novu is MIT licensed).
$.info
warn
info-version-semver
info.version should follow semantic versioning.
$.info.version
error
openapi-version-3
Use OpenAPI 3.x.
$.openapi
error
servers-https-only
All servers MUST use HTTPS.
$.servers[*].url
info
servers-novu-domain
Server URLs should target a novu.co domain.
$.servers[*].url
warn
paths-kebab-case
Path segments (excluding parameters) should use kebab-case.
$.paths.*~
warn
paths-version-prefix
Every path should be versioned (e.g. /v1/, /v2/).
$.paths.*~
error
paths-no-trailing-slash
Paths must not end with a trailing slash.
$.paths.*~
error
paths-no-query-string
Path keys must not include query strings.
$.paths.*~
error
operation-operationid-required
Every operation must declare an operationId.
$.paths.*[get,post,put,patch,delete]
warn
operation-operationid-camelcase
operationId should be camelCase (Novu uses ControllerName_methodName pattern).
$.paths.*[get,post,put,patch,delete].operationId
error
operation-summary-required
Every operation must declare a summary.
$.paths.*[get,post,put,patch,delete]
warn
operation-summary-novu-prefix
Operation summary should start with the 'Novu' provider prefix and be in Title Case.
$.paths.*[get,post,put,patch,delete].summary
warn
operation-description-required
Every operation should declare a description.
$.paths.*[get,post,put,patch,delete]
warn
operation-tags-required
Every operation should declare at least one tag.
$.paths.*[get,post,put,patch,delete]
warn
tags-title-case
Tag names should be in Title Case (matches the resource grouping in the Novu Dashboard).
$.tags[*].name
warn
parameter-description-required
Every parameter should declare a description.
$.paths.*[get,post,put,patch,delete].parameters[*]
warn
parameter-camelcase
Parameter names should be camelCase (Novu convention, e.g. subscriberId, environmentId).
$.paths.*[get,post,put,patch,delete].parameters[?(@.in!='header')].name
info
parameter-pagination-cursor-limit
Paginated list endpoints should expose cursor + limit parameters.
$.paths.*.get.parameters[?(@.name=='limit')]
warn
request-body-application-json
Request bodies should accept application/json.
$.paths.*[post,put,patch].requestBody.content
error
response-2xx-required
Every operation must define at least one 2xx response.
$.paths.*[get,post,put,patch,delete].responses
warn
response-401-required
Authenticated operations should document the 401 Unauthorized response.
$.paths.*[get,post,put,patch,delete].responses
info
response-429-required
Rate-limited operations should document the 429 Too Many Requests response.
$.paths.*[get,post,put,patch,delete].responses
warn
response-error-content-type
Error responses should return application/json.
$.paths.*[get,post,put,patch,delete].responses[?(@property.match(/^[45]/))].content
info
response-rate-limit-headers
Successful responses should expose RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset / RateLimit-Policy headers per Novu's policy.
$.paths.*[get,post,put,patch,delete].responses[?(@property.match(/^2/))].headers
info
response-idempotency-headers
Mutating operations should expose Idempotency-Key / Idempotency-Replay response headers.
$.paths.*[post,put,patch,delete].responses[?(@property.match(/^2/))].headers
warn
schema-property-camelcase
Schema property names should be camelCase (Novu DTO convention).
$.components.schemas[*].properties.*~
warn
schema-type-required
Top-level schemas must declare a type.
$.components.schemas[*]
info
schema-dto-suffix
Request/response DTO schemas should use the Dto suffix Novu adopts (RequestDto / ResponseDto / Dto).
$.components.schemas
error
security-scheme-secretkey-required
components.securitySchemes must define the secretKey ApiKey scheme used by Novu.
$.components.securitySchemes
warn
security-apikey-header
The secretKey scheme should be an ApiKey scheme delivered in the Authorization header.
$.components.securitySchemes.secretKey
error
get-no-request-body
GET operations should not declare a request body.
$.paths.*.get.requestBody
info
delete-204-or-200
DELETE operations should respond with 200 or 204.
$.paths.*.delete.responses
warn
components-schemas-defined
components.schemas should be defined.
$.components
warn
no-empty-description
Descriptions, when present, must be non-empty.
$..description
warn
servers-expected-domain
Server URLs should be on the novu.co domain.
$.servers[*].url
warn
path-params-casing
Path parameters should be camelCase (the dominant convention in this API).
$.paths[*].parameters[?(@.in=='path')].name
info
query-params-casing
Query parameters should be snake_case (the dominant convention in this API).
$.paths[*][get,post,put,patch,delete].parameters[?(@.in=='query')]
warn
schema-names-casing
Component schema names should be PascalCase (the dominant convention in this API).
$.components.schemas
info
schema-properties-casing
Schema properties should be snake_case (the dominant convention in this API).
$.components.schemas[*].properties
warn
error-schema-defined
A shared error schema (ErrorDto) should be defined for error payloads.
$.components.schemas
warn
operation-documents-400
Operations should document a 400 response (documented on 93% of this API's operations).
$.paths[*][get,post,put,patch,delete].responses
warn
operation-documents-403
Operations should document a 403 response (documented on 93% of this API's operations).
$.paths[*][get,post,put,patch,delete].responses
warn
operation-documents-404
Operations should document a 404 response (documented on 96% of this API's operations).
$.paths[*][get,post,put,patch,delete].responses
warn
operation-documents-500
Operations should document a 500 response (documented on 93% of this API's operations).
$.paths[*][get,post,put,patch,delete].responses

Spectral Ruleset

Raw ↑
# novu — Spectral ruleset (strengthened)
# Plain Spectral. Existing hand-authored rules preserved; measured rules added
# from this provider's own OpenAPI conventions by strengthen_ruleset.py,
# then self-validated against the spec.
#
# Provenance:
#   - servers-https-only: 100% of servers already https (error)
#   - servers-expected-domain: 2/2 servers on novu.co
#   - path-params-casing: camel @ 74% (n=131)
#   - query-params-casing: snake @ 53% (n=148)
#   - schema-names-casing: pascal @ 100% (n=392)
#   - schema-properties-casing: snake @ 56% (n=1774)
#   - security: global (root) — NOT emitting operation-security-required
#   - error-schema-defined: ErrorDto
#   - operation-documents-400: 93% adherence
#   - operation-documents-401: 93% adherence
#   - operation-documents-403: 93% adherence
#   - operation-documents-404: 96% adherence
#   - operation-documents-429: 93% adherence
#   - operation-documents-500: 93% adherence
#   - pagination params observed: ['limit', 'offset', 'page']
#   - merge: kept 38 existing, added 10 measured, upgraded 0
#   - added: servers-expected-domain, path-params-casing, query-params-casing, schema-names-casing, schema-properties-casing, error-schema-defined, operation-documents-400, operation-documents-403, operation-documents-404, operation-documents-500
extends:
  - spectral:oas
rules:
  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-3:
    description: Use OpenAPI 3.x.
    severity: error
    given: $.openapi
    then:
      function: pattern
      functionOptions:
        match: ^3\.
  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-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: \?
  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-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]*)*$
  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-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
  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
  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-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
  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'
  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
  servers-expected-domain:
    description: Server URLs should be on the novu.co domain.
    severity: warn
    given: $.servers[*].url
    then:
      function: pattern
      functionOptions:
        match: novu\.co
  path-params-casing:
    description: Path parameters should be camelCase (the dominant convention in this API).
    severity: warn
    given: $.paths[*].parameters[?(@.in=='path')].name
    then:
      function: casing
      functionOptions:
        type: camel
  query-params-casing:
    description: Query parameters should be snake_case (the dominant convention in this API).
    severity: info
    given: $.paths[*][get,post,put,patch,delete].parameters[?(@.in=='query')]
    then:
      field: name
      function: casing
      functionOptions:
        type: snake
  schema-names-casing:
    description: Component schema names should be PascalCase (the dominant convention in this API).
    severity: warn
    given: $.components.schemas
    then:
      field: '@key'
      function: pattern
      functionOptions:
        match: ^[A-Z][A-Za-z0-9]*$
  schema-properties-casing:
    description: Schema properties should be snake_case (the dominant convention in this API).
    severity: info
    given: $.components.schemas[*].properties
    then:
      field: '@key'
      function: casing
      functionOptions:
        type: snake
  error-schema-defined:
    description: A shared error schema (ErrorDto) should be defined for error payloads.
    severity: warn
    given: $.components.schemas
    then:
      field: ErrorDto
      function: truthy
  operation-documents-400:
    description: Operations should document a 400 response (documented on 93% of this API's operations).
    severity: warn
    given: $.paths[*][get,post,put,patch,delete].responses
    then:
      field: '400'
      function: truthy
  operation-documents-403:
    description: Operations should document a 403 response (documented on 93% of this API's operations).
    severity: warn
    given: $.paths[*][get,post,put,patch,delete].responses
    then:
      field: '403'
      function: truthy
  operation-documents-404:
    description: Operations should document a 404 response (documented on 96% of this API's operations).
    severity: warn
    given: $.paths[*][get,post,put,patch,delete].responses
    then:
      field: '404'
      function: truthy
  operation-documents-500:
    description: Operations should document a 500 response (documented on 93% of this API's operations).
    severity: warn
    given: $.paths[*][get,post,put,patch,delete].responses
    then:
      field: '500'
      function: truthy