Repository: stac-utils/stac-fastapi
Pull request #790 (2025-01-24 09:55:59)

refactored conformance classes for extensions
while working on https://github.com/stac-utils/stac-fastapi-pgstac/pull/192 I realized the `extensions` where designed first for `/search` endpoints and might lead to some issues within the application conformance classes list.

This PR aims to create extensions with correct conformances classes but also makes customization easier. 

Changelog:

  - renamed `collection_search.ConformanceClasses` -> `collection_search.CollectionSearchConformanceClasses`
  - `collection_search.CollectionSearchPostExtension.from_extension(ext)` method will now use the conformance classes from the input extensions to derived the output conformance classes.
  - added `fields.FieldsConformanceClasses` Enum
  - renamed `filter.FilterConformanceClasses.FEATURES_FILTER` -> `filter.FilterConformanceClasses.ITEMS`
  - renamed `filter.FilterConformanceClasses.ITEM_SEARCH_FILTER` -> `filter.FilterConformanceClasses.SEARCH`
  - added `filter.FilterConformanceClasses.COLLECTIONS`
  - added `filter.SearchFilterExtension`, `filter.ItemCollectionFilterExtension` and `filter.CollectionSearchFilterExtension` endpoint specific extensions
  - removed `FreeTextConformanceClasses.COLLECTIONS` and `FreeTextConformanceClasses.ITEMS` in `FreeTextExtension` and `FreeTextAdvancedExtension` default conformances classes
  - added `query.QueryConformanceClasses` Enum
  - added `SortConformanceClasses` Enum

vincentsarago: here is an example of full application https://gist.github.com/vincentsarago/4ee0fdf471cf592a8ba642dcc72304d2#file-app-py
m-mohr: Don't really have time to fully review right now, sorry. If you have specific things you want me to check though, please let me know.
vincentsarago: the changes are quite `breaking` so should we make a 5.0.0 version next? 
gadomski: > the changes are quite breaking so should we make a 5.0.0 version next?

Makes sense to me.

Patch:
diff --git a/CHANGES.md b/CHANGES.md
index aa3a1c8..3aee38e 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -2,6 +2,22 @@
 
 ## [Unreleased]
 
+### Changed
+
+- refactored conformance classes for extensions
+
+  - renamed `collection_search.ConformanceClasses` -> `collection_search.CollectionSearchConformanceClasses`
+  - removed `FREETEXT`, `FILTER`, `QUERY`, `SORT` and `FIELDS` entries from the `CollectionSearchConformanceClasses` Enum (and moved to each extension's Enum)
+  - changed `collection_search.CollectionSearchPostExtension.from_extension(ext)` to use the conformance classes from the input extensions to derive the output conformance classes.
+  - added `fields.FieldsConformanceClasses` Enum
+  - renamed `filter.FilterConformanceClasses.FEATURES_FILTER` -> `filter.FilterConformanceClasses.ITEMS`
+  - renamed `filter.FilterConformanceClasses.ITEM_SEARCH_FILTER` -> `filter.FilterConformanceClasses.SEARCH`
+  - added `filter.FilterConformanceClasses.COLLECTIONS`
+  - added `filter.SearchFilterExtension`, `filter.ItemCollectionFilterExtension` and `filter.CollectionSearchFilterExtension` endpoint specific extensions
+  - removed `FreeTextConformanceClasses.COLLECTIONS` and `FreeTextConformanceClasses.ITEMS` in `FreeTextExtension` and `FreeTextAdvancedExtension` default conformances classes
+  - added `query.QueryConformanceClasses` Enum
+  - added `SortConformanceClasses` Enum
+
 ## [4.0.1] - 2025-01-23
 
 ### Changed
diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/__init__.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/__init__.py
index 77e45d0..d6b5f75 100644
--- a/stac_fastapi/extensions/stac_fastapi/extensions/core/__init__.py
+++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/__init__.py
@@ -3,7 +3,12 @@
 from .aggregation import AggregationExtension
 from .collection_search import CollectionSearchExtension, CollectionSearchPostExtension
 from .fields import FieldsExtension
-from .filter import FilterExtension
+from .filter import (
+    CollectionSearchFilterExtension,
+    FilterExtension,
+    ItemCollectionFilterExtension,
+    SearchFilterExtension,
+)
 from .free_text import FreeTextAdvancedExtension, FreeTextExtension
 from .pagination import (
     OffsetPaginationExtension,
@@ -28,4 +33,7 @@ __all__ = (
     "TransactionExtension",
     "CollectionSearchExtension",
     "CollectionSearchPostExtension",
+    "SearchFilterExtension",
+    "ItemCollectionFilterExtension",
+    "CollectionSearchFilterExtension",
 )
diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/aggregation/__init__.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/aggregation/__init__.py
index 2a7fc7a..c7dceaf 100644
--- a/stac_fastapi/extensions/stac_fastapi/extensions/core/aggregation/__init__.py
+++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/aggregation/__init__.py
@@ -1,5 +1,5 @@
 """Aggregation extension module."""
 
-from .aggregation import AggregationExtension
+from .aggregation import AggregationConformanceClasses, AggregationExtension
 
-__all__ = ["AggregationExtension"]
+__all__ = ["AggregationExtension", "AggregationConformanceClasses"]
diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/collection_search/__init__.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/collection_search/__init__.py
index eed6d50..2565bec 100644
--- a/stac_fastapi/extensions/stac_fastapi/extensions/core/collection_search/__init__.py
+++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/collection_search/__init__.py
@@ -1,13 +1,13 @@
 """Collection-Search extension module."""
 
 from .collection_search import (
+    CollectionSearchConformanceClasses,
     CollectionSearchExtension,
     CollectionSearchPostExtension,
-    ConformanceClasses,
 )
 
 __all__ = [
     "CollectionSearchExtension",
     "CollectionSearchPostExtension",
-    "ConformanceClasses",
+    "CollectionSearchConformanceClasses",
 ]
diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/collection_search/collection_search.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/collection_search/collection_search.py
index 2bd7ac2..e5ab77b 100644
--- a/stac_fastapi/extensions/stac_fastapi/extensions/core/collection_search/collection_search.py
+++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/collection_search/collection_search.py
@@ -17,7 +17,7 @@ from .client import AsyncBaseCollectionSearchClient, BaseCollectionSearchClient
 from .request import BaseCollectionSearchGetRequest, BaseCollectionSearchPostRequest
 
 
-class ConformanceClasses(str, Enum):
+class CollectionSearchConformanceClasses(str, Enum):
     """Conformance classes for the Collection-Search extension.
 
     See
@@ -26,11 +26,6 @@ class ConformanceClasses(str, Enum):
 
     COLLECTIONSEARCH = "https://api.stacspec.org/v1.0.0-rc.1/collection-search"
     BASIS = "http://www.opengis.net/spec/ogcapi-common-2/1.0/conf/simple-query"
-    FREETEXT = "https://api.stacspec.org/v1.0.0-rc.1/collection-search#free-text"
-    FILTER = "https://api.stacspec.org/v1.0.0-rc.1/collection-search#filter"
-    QUERY = "https://api.stacspec.org/v1.0.0-rc.1/collection-search#query"
-    SORT = "https://api.stacspec.org/v1.0.0-rc.1/collection-search#sort"
-    FIELDS = "https://api.stacspec.org/v1.0.0-rc.1/collection-search#fields"
 
 
 @attr.s
@@ -56,7 +51,10 @@ class CollectionSearchExtension(ApiExtension):
     POST = None
 
     conformance_classes: List[str] = attr.ib(
-        default=[ConformanceClasses.COLLECTIONSEARCH, ConformanceClasses.BASIS]
+        default=[
+            CollectionSearchConformanceClasses.COLLECTIONSEARCH,
+            CollectionSearchConformanceClasses.BASIS,
+        ]
     )
     schema_href: Optional[str] = attr.ib(default=None)
 
@@ -78,21 +76,13 @@ class CollectionSearchExtension(ApiExtension):
         schema_href: Optional[str] = None,
     ) -> "CollectionSearchExtension":
         """Create CollectionSearchExtension object from extensions."""
-        known_extension_conformances = {
-            "FreeTextExtension": ConformanceClasses.FREETEXT,
-            "FreeTextAdvancedExtension": ConformanceClasses.FREETEXT,
-            "QueryExtension": ConformanceClasses.QUERY,
-            "SortExtension": ConformanceClasses.SORT,
-            "FieldsExtension": ConformanceClasses.FIELDS,
-            "FilterExtension": ConformanceClasses.FILTER,
-        }
+
         conformance_classes = [
-            ConformanceClasses.COLLECTIONSEARCH,
-            ConformanceClasses.BASIS,
+            CollectionSearchConformanceClasses.COLLECTIONSEARCH,
+            CollectionSearchConformanceClasses.BASIS,
         ]
         for ext in extensions:
-            if conf := known_extension_conformances.get(ext.__class__.__name__, None):
-                conformance_classes.append(conf)
+            conformance_classes.extend(ext.conformance_classes)
 
         get_request_model = create_request_model(
             model_name="CollectionsGetRequest",
@@ -128,7 +118,10 @@ class CollectionSearchPostExtension(CollectionSearchExtension):
     client: Union[AsyncBaseCollectionSearchClient, BaseCollectionSearchClient] = attr.ib()
     settings: ApiSettings = attr.ib()
     conformance_classes: List[str] = attr.ib(
-        default=[ConformanceClasses.COLLECTIONSEARCH, ConformanceClasses.BASIS]
+        default=[
+            CollectionSearchConformanceClasses.COLLECTIONSEARCH,
+            CollectionSearchConformanceClasses.BASIS,
+        ]
     )
     schema_href: Optional[str] = attr.ib(default=None)
     router: APIRouter = attr.ib(factory=APIRouter)
@@ -180,21 +173,12 @@ class CollectionSearchPostExtension(CollectionSearchExtension):
         router: Optional[APIRouter] = None,
     ) -> "CollectionSearchPostExtension":
         """Create CollectionSearchPostExtension object from extensions."""
-        known_extension_conformances = {
-            "FreeTextExtension": ConformanceClasses.FREETEXT,
-            "FreeTextAdvancedExtension": ConformanceClasses.FREETEXT,
-            "QueryExtension": ConformanceClasses.QUERY,
-            "SortExtension": ConformanceClasses.SORT,
-            "FieldsExtension": ConformanceClasses.FIELDS,
-            "FilterExtension": ConformanceClasses.FILTER,
-        }
         conformance_classes = [
-            ConformanceClasses.COLLECTIONSEARCH,
-            ConformanceClasses.BASIS,
+            CollectionSearchConformanceClasses.COLLECTIONSEARCH,
+            CollectionSearchConformanceClasses.BASIS,
         ]
         for ext in extensions:
-            if conf := known_extension_conformances.get(ext.__class__.__name__, None):
-                conformance_classes.append(conf)
+            conformance_classes.extend(ext.conformance_classes)
 
         get_request_model = create_request_model(
             model_name="CollectionsGetRequest",
diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/fields/__init__.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/fields/__init__.py
index 087d01b..da67b14 100644
--- a/stac_fastapi/extensions/stac_fastapi/extensions/core/fields/__init__.py
+++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/fields/__init__.py
@@ -1,5 +1,5 @@
 """Fields extension module."""
 
-from .fields import FieldsExtension
+from .fields import FieldsConformanceClasses, FieldsExtension
 
-__all__ = ["FieldsExtension"]
+__all__ = ["FieldsExtension", "FieldsConformanceClasses"]
diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/fields/fields.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/fields/fields.py
index 90b4b26..0b6e417 100644
--- a/stac_fastapi/extensions/stac_fastapi/extensions/core/fields/fields.py
+++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/fields/fields.py
@@ -1,5 +1,6 @@
 """Fields extension."""
 
+from enum import Enum
 from typing import List, Optional
 
 import attr
@@ -10,6 +11,18 @@ from stac_fastapi.types.extension import ApiExtension
 from .request import FieldsExtensionGetRequest, FieldsExtensionPostRequest
 
 
+class FieldsConformanceClasses(str, Enum):
+    """Conformance classes for the Fields extension.
+
+    See https://github.com/stac-api-extensions/fields
+
+    """
+
+    SEARCH = "https://api.stacspec.org/v1.0.0/item-search#fields"
+    ITEMS = "https://api.stacspec.org/v1.0.0/ogcapi-features#fields"
+    COLLECTIONS = "https://api.stacspec.org/v1.0.0-rc.1/collection-search#fields"
+
+
 @attr.s
 class FieldsExtension(ApiExtension):
     """Fields Extension.
@@ -33,7 +46,9 @@ class FieldsExtension(ApiExtension):
     POST = FieldsExtensionPostRequest
 
     conformance_classes: List[str] = attr.ib(
-        factory=lambda: ["https://api.stacspec.org/v1.0.0/item-search#fields"]
+        factory=lambda: [
+            FieldsConformanceClasses.SEARCH,
+        ]
     )
     schema_href: Optional[str] = attr.ib(default=None)
 
diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/filter/__init__.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/filter/__init__.py
index 256f3e0..dbbf2b1 100644
--- a/stac_fastapi/extensions/stac_fastapi/extensions/core/filter/__init__.py
+++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/filter/__init__.py
@@ -1,5 +1,17 @@
 """Filter extension module."""
 
-from .filter import FilterExtension
+from .filter import (
+    CollectionSearchFilterExtension,
+    FilterConformanceClasses,
+    FilterExtension,
+    ItemCollectionFilterExtension,
+    SearchFilterExtension,
+)
 
-__all__ = ["FilterExtension"]
+__all__ = [
+    "FilterConformanceClasses",
+    "FilterExtension",
+    "SearchFilterExtension",
+    "ItemCollectionFilterExtension",
+    "CollectionSearchFilterExtension",
+]
diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/filter/filter.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/filter/filter.py
index cd9463e..d182dcf 100644
--- a/stac_fastapi/extensions/stac_fastapi/extensions/core/filter/filter.py
+++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/filter/filter.py
@@ -23,10 +23,11 @@ class FilterConformanceClasses(str, Enum):
     """
 
     FILTER = "http://www.opengis.net/spec/ogcapi-features-3/1.0/conf/filter"
-    FEATURES_FILTER = (
-        "http://www.opengis.net/spec/ogcapi-features-3/1.0/conf/features-filter"
-    )
-    ITEM_SEARCH_FILTER = "https://api.stacspec.org/v1.0.0-rc.2/item-search#filter"
+
+    SEARCH = "https://api.stacspec.org/v1.0.0-rc.2/item-search#filter"
+    ITEMS = "http://www.opengis.net/spec/ogcapi-features-3/1.0/conf/features-filter"
+    COLLECTIONS = "https://api.stacspec.org/v1.0.0-rc.1/collection-search#filter"
+
     CQL2_TEXT = "http://www.opengis.net/spec/cql2/1.0/conf/cql2-text"
     CQL2_JSON = "http://www.opengis.net/spec/cql2/1.0/conf/cql2-json"
     BASIC_CQL2 = "http://www.opengis.net/spec/cql2/1.0/conf/basic-cql2"
@@ -73,8 +74,8 @@ class FilterExtension(ApiExtension):
     conformance_classes: List[str] = attr.ib(
         default=[
             FilterConformanceClasses.FILTER,
-            FilterConformanceClasses.FEATURES_FILTER,
-            FilterConformanceClasses.ITEM_SEARCH_FILTER,
+            FilterConformanceClasses.SEARCH,
+            FilterConformanceClasses.ITEMS,
             FilterConformanceClasses.BASIC_CQL2,
             FilterConformanceClasses.CQL2_JSON,
             FilterConformanceClasses.CQL2_TEXT,
@@ -124,3 +125,112 @@ class FilterExtension(ApiExtension):
             endpoint=create_async_endpoint(self.client.get_queryables, CollectionUri),
         )
         app.include_router(self.router, tags=["Filter Extension"])
+
+
+@attr.s
+class SearchFilterExtension(FilterExtension):
+    """Item Search Filter Extension."""
+
+    conformance_classes: List[str] = attr.ib(
+        default=[
+            FilterConformanceClasses.FILTER,
+            FilterConformanceClasses.SEARCH,
+            FilterConformanceClasses.BASIC_CQL2,
+            FilterConformanceClasses.CQL2_JSON,
+            FilterConformanceClasses.CQL2_TEXT,
+        ]
+    )
+
+    def register(self, app: FastAPI) -> None:
+        """Register the extension with a FastAPI application.
+
+        Args:
+            app: target FastAPI application.
+
+        Returns:
+            None
+        """
+        self.router.prefix = app.state.router_prefix
+        self.router.add_api_route(
+            name="Queryables",
+            path="/queryables",
+            methods=["GET"],
+            responses={
+                200: {
+                    "content": {
+                        "application/schema+json": {},
+                    },
+                    # TODO: add output model in stac-pydantic
+                },
+            },
+            response_class=self.response_class,
+            endpoint=create_async_endpoint(self.client.get_queryables, EmptyRequest),
+        )
+        app.include_router(self.router, tags=["Filter Extension"])
+
+
+@attr.s
+class ItemCollectionFilterExtension(FilterExtension):
+    """Item Collection Filter Extension."""
+
+    conformance_classes: List[str] = attr.ib(
+        default=[
+            FilterConformanceClasses.FILTER,
+            FilterConformanceClasses.ITEMS,
+            FilterConformanceClasses.BASIC_CQL2,
+            FilterConformanceClasses.CQL2_JSON,
+            FilterConformanceClasses.CQL2_TEXT,
+        ]
+    )
+
+    def register(self, app: FastAPI) -> None:
+        """Register the extension with a FastAPI application.
+
+        Args:
+            app: target FastAPI application.
+
+        Returns:
+            None
+        """
+        self.router.add_api_route(
+            name="Collection Queryables",
+            path="/collections/{collection_id}/queryables",
+            methods=["GET"],
+            responses={
+                200: {
+                    "content": {
+                        "application/schema+json": {},
+                    },
+                    # TODO: add output model in stac-pydantic
+                },
+            },
+            response_class=self.response_class,
+            endpoint=create_async_endpoint(self.client.get_queryables, CollectionUri),
+        )
+        app.include_router(self.router, tags=["Filter Extension"])
+
+
+@attr.s
+class CollectionSearchFilterExtension(FilterExtension):
+    """Collection Search Filter Extension."""
+
+    conformance_classes: List[str] = attr.ib(
+        default=[
+            FilterConformanceClasses.FILTER,
+            FilterConformanceClasses.COLLECTIONS,
+            FilterConformanceClasses.BASIC_CQL2,
+            FilterConformanceClasses.CQL2_JSON,
+            FilterConformanceClasses.CQL2_TEXT,
+        ]
+    )
+
+    def register(self, app: FastAPI) -> None:
+        """Register the extension with a FastAPI application.
+
+        Args:
+            app: target FastAPI application.
+
+        Returns:
+            None
+        """
+        pass
diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/free_text/free_text.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/free_text/free_text.py
index 8b61b32..67aaa2b 100644
--- a/stac_fastapi/extensions/stac_fastapi/extensions/core/free_text/free_text.py
+++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/free_text/free_text.py
@@ -25,19 +25,19 @@ class FreeTextConformanceClasses(str, Enum):
 
     # https://github.com/stac-api-extensions/freetext-search?tab=readme-ov-file#basic
     SEARCH = "https://api.stacspec.org/v1.0.0-rc.1/item-search#free-text"
-    COLLECTIONS = "https://api.stacspec.org/v1.0.0-rc.1/collection-search#free-text"
     ITEMS = "https://api.stacspec.org/v1.0.0-rc.1/ogcapi-features#free-text"
+    COLLECTIONS = "https://api.stacspec.org/v1.0.0-rc.1/collection-search#free-text"
 
     # https://github.com/stac-api-extensions/freetext-search?tab=readme-ov-file#advanced
     SEARCH_ADVANCED = (
         "https://api.stacspec.org/v1.0.0-rc.1/item-search#advanced-free-text"
     )
-    COLLECTIONS_ADVANCED = (
-        "https://api.stacspec.org/v1.0.0-rc.1/collection-search#advanced-free-text"
-    )
     ITEMS_ADVANCED = (
         "https://api.stacspec.org/v1.0.0-rc.1/ogcapi-features#advanced-free-text"
     )
+    COLLECTIONS_ADVANCED = (
+        "https://api.stacspec.org/v1.0.0-rc.1/collection-search#advanced-free-text"
+    )
 
 
 @attr.s
@@ -57,8 +57,6 @@ class FreeTextExtension(ApiExtension):
     conformance_classes: List[str] = attr.ib(
         default=[
             FreeTextConformanceClasses.SEARCH,
-            FreeTextConformanceClasses.COLLECTIONS,
-            FreeTextConformanceClasses.ITEMS,
         ]
     )
     schema_href: Optional[str] = attr.ib(default=None)
@@ -92,8 +90,6 @@ class FreeTextAdvancedExtension(ApiExtension):
     conformance_classes: List[str] = attr.ib(
         default=[
             FreeTextConformanceClasses.SEARCH_ADVANCED,
-            FreeTextConformanceClasses.COLLECTIONS_ADVANCED,
-            FreeTextConformanceClasses.ITEMS_ADVANCED,
         ]
     )
     schema_href: Optional[str] = attr.ib(default=None)
diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/query/__init__.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/query/__init__.py
index 5bbe705..1be29a5 100644
--- a/stac_fastapi/extensions/stac_fastapi/extensions/core/query/__init__.py
+++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/query/__init__.py
@@ -1,5 +1,5 @@
 """Query extension module."""
 
-from .query import QueryExtension
+from .query import QueryConformanceClasses, QueryExtension
 
-__all__ = ["QueryExtension"]
+__all__ = ["QueryExtension", "QueryConformanceClasses"]
diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/query/query.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/query/query.py
index 472c385..9f4c8cb 100644
--- a/stac_fastapi/extensions/stac_fastapi/extensions/core/query/query.py
+++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/query/query.py
@@ -1,5 +1,6 @@
 """Query extension."""
 
+from enum import Enum
 from typing import List, Optional
 
 import attr
@@ -10,6 +11,17 @@ from stac_fastapi.types.extension import ApiExtension
 from .request import QueryExtensionGetRequest, QueryExtensionPostRequest
 
 
+class QueryConformanceClasses(str, Enum):
+    """Conformance classes for the Query extension.
+
+    See https://github.com/stac-api-extensions/query
+    """
+
+    SEARCH = "https://api.stacspec.org/v1.0.0/item-search#query"
+    ITEMS = "https://api.stacspec.org/v1.0.0/ogcapi-features#query"
+    COLLECTIONS = "https://api.stacspec.org/v1.0.0-rc.1/collection-search#query"
+
+
 @attr.s
 class QueryExtension(ApiExtension):
     """Query Extension.
@@ -24,7 +36,9 @@ class QueryExtension(ApiExtension):
     POST = QueryExtensionPostRequest
 
     conformance_classes: List[str] = attr.ib(
-        factory=lambda: ["https://api.stacspec.org/v1.0.0/item-search#query"]
+        factory=lambda: [
+            QueryConformanceClasses.SEARCH,
+        ]
     )
     schema_href: Optional[str] = attr.ib(default=None)
 
diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/sort/__init__.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/sort/__init__.py
index b6996b0..561dbf4 100644
--- a/stac_fastapi/extensions/stac_fastapi/extensions/core/sort/__init__.py
+++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/sort/__init__.py
@@ -1,5 +1,5 @@
 """Sort extension module."""
 
-from .sort import SortExtension
+from .sort import SortConformanceClasses, SortExtension
 
-__all__ = ["SortExtension"]
+__all__ = ["SortExtension", "SortConformanceClasses"]
diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/sort/sort.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/sort/sort.py
index 4b27d8d..7798471 100644
--- a/stac_fastapi/extensions/stac_fastapi/extensions/core/sort/sort.py
+++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/sort/sort.py
@@ -1,5 +1,6 @@
 """Sort extension."""
 
+from enum import Enum
 from typing import List, Optional
 
 import attr
@@ -10,6 +11,18 @@ from stac_fastapi.types.extension import ApiExtension
 from .request import SortExtensionGetRequest, SortExtensionPostRequest
 
 
+class SortConformanceClasses(str, Enum):
+    """Conformance classes for the Sort extension.
+
+    See https://github.com/stac-api-extensions/sort
+
+    """
+
+    SEARCH = "https://api.stacspec.org/v1.0.0/item-search#sort"
+    ITEMS = "https://api.stacspec.org/v1.0.0/ogcapi-features#sort"
+    COLLECTIONS = "https://api.stacspec.org/v1.0.0-rc.1/collection-search#sort"
+
+
 @attr.s
 class SortExtension(ApiExtension):
     """Sort Extension.
@@ -23,7 +36,9 @@ class SortExtension(ApiExtension):
     POST = SortExtensionPostRequest
 
     conformance_classes: List[str] = attr.ib(
-        factory=lambda: ["https://api.stacspec.org/v1.0.0/item-search#sort"]
+        factory=lambda: [
+            SortConformanceClasses.SEARCH,
+        ]
     )
     schema_href: Optional[str] = attr.ib(default=None)
 
diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py
index 4e940a0..246be29 100644
--- a/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py
+++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py
@@ -1,5 +1,6 @@
 """Transaction extension."""
 
+from enum import Enum
 from typing import List, Optional, Type, Union
 
 import attr
@@ -16,6 +17,17 @@ from stac_fastapi.types.core import AsyncBaseTransactionsClient, BaseTransaction
 from stac_fastapi.types.extension import ApiExtension
 
 
+class TransactionConformanceClasses(str, Enum):
+    """Conformance classes for the Transaction extension.
+
+    See https://github.com/stac-api-extensions/transaction
+
+    """
+
+    ITEMS = "https://api.stacspec.org/v1.0.0/ogcapi-features/extensions/transaction"
+    COLLECTIONS = "https://api.stacspec.org/v1.0.0/collections/extensions/transaction"
+
+
 @attr.s
 class PostItem(CollectionUri):
     """Create Item."""
@@ -62,8 +74,8 @@ class TransactionExtension(ApiExtension):
     settings: ApiSettings = attr.ib()
     conformance_classes: List[str] = attr.ib(
         factory=lambda: [
-            "https://api.stacspec.org/v1.0.0/ogcapi-features/extensions/transaction",
-            "https://api.stacspec.org/v1.0.0/collections/extensions/transaction",
+            TransactionConformanceClasses.ITEMS,
+            TransactionConformanceClasses.COLLECTIONS,
         ]
     )
     schema_href: Optional[str] = attr.ib(default=None)


Test patch:
diff --git a/stac_fastapi/extensions/tests/test_collection_search.py b/stac_fastapi/extensions/tests/test_collection_search.py
index 83de85e..4c5f641 100644
--- a/stac_fastapi/extensions/tests/test_collection_search.py
+++ b/stac_fastapi/extensions/tests/test_collection_search.py
@@ -10,15 +10,17 @@ from stac_fastapi.api.models import create_request_model
 from stac_fastapi.extensions.core import (
     AggregationExtension,
     CollectionSearchExtension,
+    CollectionSearchFilterExtension,
     CollectionSearchPostExtension,
     FieldsExtension,
-    FilterExtension,
     FreeTextAdvancedExtension,
     FreeTextExtension,
     QueryExtension,
     SortExtension,
 )
-from stac_fastapi.extensions.core.collection_search import ConformanceClasses
+from stac_fastapi.extensions.core.collection_search import (
+    CollectionSearchConformanceClasses,
+)
 from stac_fastapi.extensions.core.collection_search.client import (
     BaseCollectionSearchClient,
 )
@@ -26,22 +28,27 @@ from stac_fastapi.extensions.core.collection_search.request import (
     BaseCollectionSearchGetRequest,
     BaseCollectionSearchPostRequest,
 )
+from stac_fastapi.extensions.core.fields import FieldsConformanceClasses
 from stac_fastapi.extensions.core.fields.request import (
     FieldsExtensionGetRequest,
     FieldsExtensionPostRequest,
 )
+from stac_fastapi.extensions.core.filter import FilterConformanceClasses
 from stac_fastapi.extensions.core.filter.request import (
     FilterExtensionGetRequest,
     FilterExtensionPostRequest,
 )
+from stac_fastapi.extensions.core.free_text import FreeTextConformanceClasses
 from stac_fastapi.extensions.core.free_text.request import (
     FreeTextExtensionGetRequest,
     FreeTextExtensionPostRequest,
 )
+from stac_fastapi.extensions.core.query import QueryConformanceClasses
 from stac_fastapi.extensions.core.query.request import (
     QueryExtensionGetRequest,
     QueryExtensionPostRequest,
 )
+from stac_fastapi.extensions.core.sort import SortConformanceClasses
 from stac_fastapi.extensions.core.sort.request import (
     SortExtensionGetRequest,
     SortExtensionPostRequest,
@@ -148,13 +155,13 @@ def test_collection_search_extension_models():
             CollectionSearchExtension(
                 GET=collections_get_request_model,
                 conformance_classes=[
-                    ConformanceClasses.COLLECTIONSEARCH,
-                    ConformanceClasses.BASIS,
-                    ConformanceClasses.FREETEXT,
-                    ConformanceClasses.FILTER,
-                    ConformanceClasses.QUERY,
-                    ConformanceClasses.SORT,
-                    ConformanceClasses.FIELDS,
+                    CollectionSearchConformanceClasses.COLLECTIONSEARCH,
+                    CollectionSearchConformanceClasses.BASIS,
+                    FieldsConformanceClasses.COLLECTIONS,
+                    FilterConformanceClasses.COLLECTIONS,
+                    FreeTextConformanceClasses.COLLECTIONS,
+                    QueryConformanceClasses.COLLECTIONS,
+                    SortConformanceClasses.COLLECTIONS,
                 ],
             )
         ],
@@ -311,13 +318,13 @@ def test_collection_search_extension_post_models():
                 GET=get_request_model,
                 POST=post_request_model,
                 conformance_classes=[
-                    ConformanceClasses.COLLECTIONSEARCH,
-                    ConformanceClasses.BASIS,
-                    ConformanceClasses.FREETEXT,
-                    ConformanceClasses.FILTER,
-                    ConformanceClasses.QUERY,
-                    ConformanceClasses.SORT,
-                    ConformanceClasses.FIELDS,
+                    CollectionSearchConformanceClasses.COLLECTIONSEARCH,
+                    CollectionSearchConformanceClasses.BASIS,
+                    FieldsConformanceClasses.COLLECTIONS,
+                    FilterConformanceClasses.COLLECTIONS,
+                    FreeTextConformanceClasses.COLLECTIONS,
+                    QueryConformanceClasses.COLLECTIONS,
+                    SortConformanceClasses.COLLECTIONS,
                 ],
             )
         ],
@@ -403,19 +410,23 @@ def test_collection_search_extension_post_models():
     [
         # with FreeTextExtension
         [
-            FieldsExtension(),
-            FilterExtension(),
-            FreeTextExtension(),
-            QueryExtension(),
-            SortExtension(),
+            FieldsExtension(conformance_classes=[FieldsConformanceClasses.COLLECTIONS]),
+            CollectionSearchFilterExtension(),
+            FreeTextExtension(
+                conformance_classes=[FreeTextConformanceClasses.COLLECTIONS]
+            ),
+            QueryExtension(conformance_classes=[QueryConformanceClasses.COLLECTIONS]),
+            SortExtension(conformance_classes=[SortConformanceClasses.COLLECTIONS]),
         ],
         # with FreeTextAdvancedExtension
         [
-            FieldsExtension(),
-            FilterExtension(),
-            FreeTextAdvancedExtension(),
-            QueryExtension(),
-            SortExtension(),
+            FieldsExtension(conformance_classes=[FieldsConformanceClasses.COLLECTIONS]),
+            CollectionSearchFilterExtension(),
+            FreeTextAdvancedExtension(
+                conformance_classes=[FreeTextConformanceClasses.COLLECTIONS]
+            ),
+            QueryExtension(conformance_classes=[QueryConformanceClasses.COLLECTIONS]),
+            SortExtension(conformance_classes=[SortConformanceClasses.COLLECTIONS]),
         ],
     ],
 )
@@ -436,15 +447,20 @@ def test_from_extensions_methods(extensions):
     assert hasattr(collection_search, "q")
     assert hasattr(collection_search, "sortby")
     assert hasattr(collection_search, "filter_expr")
-    assert ext.conformance_classes == [
-        ConformanceClasses.COLLECTIONSEARCH,
-        ConformanceClasses.BASIS,
-        ConformanceClasses.FIELDS,
-        ConformanceClasses.FILTER,
-        ConformanceClasses.FREETEXT,
-        ConformanceClasses.QUERY,
-        ConformanceClasses.SORT,
-    ]
+    for conf in [
+        CollectionSearchConformanceClasses.COLLECTIONSEARCH,
+        CollectionSearchConformanceClasses.BASIS,
+        FieldsConformanceClasses.COLLECTIONS,
+        FilterConformanceClasses.COLLECTIONS,
+        FilterConformanceClasses.FILTER,
+        FilterConformanceClasses.BASIC_CQL2,
+        FilterConformanceClasses.CQL2_JSON,
+        FilterConformanceClasses.CQL2_TEXT,
+        FreeTextConformanceClasses.COLLECTIONS,
+        QueryConformanceClasses.COLLECTIONS,
+        SortConformanceClasses.COLLECTIONS,
+    ]:
+        assert conf in ext.conformance_classes
 
     ext = CollectionSearchPostExtension.from_extensions(
         extensions,
@@ -460,15 +476,20 @@ def test_from_extensions_methods(extensions):
     assert hasattr(collection_search, "q")
     assert hasattr(collection_search, "sortby")
     assert hasattr(collection_search, "filter_expr")
-    assert ext.conformance_classes == [
-        ConformanceClasses.COLLECTIONSEARCH,
-        ConformanceClasses.BASIS,
-        ConformanceClasses.FIELDS,
-        ConformanceClasses.FILTER,
-        ConformanceClasses.FREETEXT,
-        ConformanceClasses.QUERY,
-        ConformanceClasses.SORT,
-    ]
+    for conf in [
+        CollectionSearchConformanceClasses.COLLECTIONSEARCH,
+        CollectionSearchConformanceClasses.BASIS,
+        FieldsConformanceClasses.COLLECTIONS,
+        FilterConformanceClasses.COLLECTIONS,
+        FilterConformanceClasses.FILTER,
+        FilterConformanceClasses.BASIC_CQL2,
+        FilterConformanceClasses.CQL2_JSON,
+        FilterConformanceClasses.CQL2_TEXT,
+        FreeTextConformanceClasses.COLLECTIONS,
+        QueryConformanceClasses.COLLECTIONS,
+        SortConformanceClasses.COLLECTIONS,
+    ]:
+        assert conf in ext.conformance_classes
 
 
 def test_from_extensions_methods_invalid():
@@ -486,10 +507,11 @@ def test_from_extensions_methods_invalid():
     assert hasattr(collection_search, "datetime")
     assert hasattr(collection_search, "limit")
     assert hasattr(collection_search, "aggregations")
-    assert ext.conformance_classes == [
-        ConformanceClasses.COLLECTIONSEARCH,
-        ConformanceClasses.BASIS,
-    ]
+    for conf in [
+        CollectionSearchConformanceClasses.COLLECTIONSEARCH,
+        CollectionSearchConformanceClasses.BASIS,
+    ]:
+        assert conf in ext.conformance_classes
 
     ext = CollectionSearchPostExtension.from_extensions(
         extensions,
@@ -502,7 +524,8 @@ def test_from_extensions_methods_invalid():
     assert hasattr(collection_search, "datetime")
     assert hasattr(collection_search, "limit")
     assert hasattr(collection_search, "aggregations")
-    assert ext.conformance_classes == [
-        ConformanceClasses.COLLECTIONSEARCH,
-        ConformanceClasses.BASIS,
-    ]
+    for conf in [
+        CollectionSearchConformanceClasses.COLLECTIONSEARCH,
+        CollectionSearchConformanceClasses.BASIS,
+    ]:
+        assert conf in ext.conformance_classes
diff --git a/stac_fastapi/extensions/tests/test_filter.py b/stac_fastapi/extensions/tests/test_filter.py
index 32152c4..bc8b902 100644
--- a/stac_fastapi/extensions/tests/test_filter.py
+++ b/stac_fastapi/extensions/tests/test_filter.py
@@ -6,6 +6,11 @@ from starlette.testclient import TestClient
 from stac_fastapi.api.app import StacApi
 from stac_fastapi.api.models import create_get_request_model, create_post_request_model
 from stac_fastapi.extensions.core import FilterExtension
+from stac_fastapi.extensions.core.filter import (
+    CollectionSearchFilterExtension,
+    ItemCollectionFilterExtension,
+    SearchFilterExtension,
+)
 from stac_fastapi.types.config import ApiSettings
 from stac_fastapi.types.core import BaseCoreClient
 
@@ -31,7 +36,7 @@ class DummyCoreClient(BaseCoreClient):
         raise NotImplementedError
 
 
-@pytest.fixture
+@pytest.fixture(autouse=True)
 def client() -> Iterator[TestClient]:
     settings = ApiSettings()
     extensions = [FilterExtension()]
@@ -46,8 +51,63 @@ def client() -> Iterator[TestClient]:
         yield client
 
 
-def test_search_filter_post_filter_lang_default(client: TestClient):
+@pytest.fixture(autouse=True)
+def client_multit_ext() -> Iterator[TestClient]:
+    settings = ApiSettings()
+    extensions = [
+        SearchFilterExtension(),
+        ItemCollectionFilterExtension(),
+        # Technically `CollectionSearchFilterExtension`
+        # shouldn't be registered to the application but to the collection-search class
+        CollectionSearchFilterExtension(),
+    ]
+
+    api = StacApi(
+        settings=settings,
+        client=DummyCoreClient(),
+        extensions=extensions,
+        search_get_request_model=create_get_request_model([SearchFilterExtension()]),
+        search_post_request_model=create_post_request_model([SearchFilterExtension()]),
+    )
+    with TestClient(api.app) as client:
+        yield client
+
+
+@pytest.mark.parametrize("client_name", ["client", "client_multit_ext"])
+def test_filter_endpoints_conformances(client_name, request):
+    """Make sure conformances classes are set."""
+    client = request.getfixturevalue(client_name)
+
+    response = client.get("/conformance")
+    assert response.is_success, response.json()
+    response_dict = response.json()
+    conf = response_dict["conformsTo"]
+    assert (
+        "http://www.opengis.net/spec/ogcapi-features-3/1.0/conf/features-filter" in conf
+    )
+    assert "https://api.stacspec.org/v1.0.0-rc.2/item-search#filter" in conf
+    assert client.get("/queryables").is_success
+    assert client.get("/collections/collection_id/queryables").is_success
+
+
+def test_filter_conformances_collection_search(client_multit_ext):
+    """Make sure conformances classes are set."""
+    response = client_multit_ext.get("/conformance")
+    assert response.is_success, response.json()
+    response_dict = response.json()
+    conf = response_dict["conformsTo"]
+    assert (
+        "http://www.opengis.net/spec/ogcapi-features-3/1.0/conf/features-filter" in conf
+    )
+    assert "https://api.stacspec.org/v1.0.0-rc.2/item-search#filter" in conf
+    assert "https://api.stacspec.org/v1.0.0-rc.1/collection-search#filter" in conf
+
+
+@pytest.mark.parametrize("client_name", ["client", "client_multit_ext"])
+def test_search_filter_post_filter_lang_default(client_name, request):
     """Test search POST endpoint with filter ext."""
+    client = request.getfixturevalue(client_name)
+
     response = client.post(
         "/search",
         json={
@@ -61,8 +121,11 @@ def test_search_filter_post_filter_lang_default(client: TestClient):
     assert response_dict["filter_lang"] == "cql2-json"
 
 
-def test_search_filter_post_filter_lang_non_default(client: TestClient):
+@pytest.mark.parametrize("client_name", ["client", "client_multit_ext"])
+def test_search_filter_post_filter_lang_non_default(client_name, request):
     """Test search POST endpoint with filter ext."""
+    client = request.getfixturevalue(client_name)
+
     filter_lang_value = "cql-json"
     response = client.post(
         "/search",
@@ -78,8 +141,11 @@ def test_search_filter_post_filter_lang_non_default(client: TestClient):
     assert response_dict["filter_lang"] == filter_lang_value
 
 
-def test_search_filter_get(client: TestClient):
+@pytest.mark.parametrize("client_name", ["client", "client_multit_ext"])
+def test_search_filter_get(client_name, request):
     """Test search GET endpoint with filter ext."""
+    client = request.getfixturevalue(client_name)
+
     response = client.get(
         "/search",
         params={