diff --git a/rbtools/api/resource/base.py b/rbtools/api/resource/base.py
index 81bacc6c20a12666cf1f06c6e8780ebb22721e5b..375263aafb07d7e9aa8689eed44ddd78344db8f0 100644
--- a/rbtools/api/resource/base.py
+++ b/rbtools/api/resource/base.py
@@ -9,10 +9,12 @@ from __future__ import annotations
 
 import copy
 import json
+import logging
 from collections.abc import Iterator, MutableMapping
 from functools import update_wrapper, wraps
-from typing import (Any, Callable, ClassVar, Final, Optional, NoReturn,
-                    TypeVar, TYPE_CHECKING, Union, cast)
+from typing import (Any, Callable, ClassVar, Final, Generic, Literal,
+                    Optional, NoReturn, TypeVar, TYPE_CHECKING, Union, cast,
+                    overload)
 from urllib.parse import urljoin
 
 from typelets.json import JSONDict, JSONValue
@@ -25,6 +27,9 @@ if TYPE_CHECKING:
     from rbtools.api.transport import Transport
 
 
+logger = logging.getLogger(__name__)
+
+
 #: Map from MIME type to resource class.
 RESOURCE_MAP: dict[str, type[Resource]] = {}
 
@@ -616,22 +621,50 @@ class Resource:
         else:
             self._expanded_info = {}
 
+        members = set(dir(self))
+
         # Add a method for each supported REST operation, and
         # for retrieving 'self'.
         for link, method in SPECIAL_LINKS.items():
             if link in self._links and method[1]:
-                setattr(self,
-                        method[0],
-                        lambda resource=self, meth=method[1], **kwargs: (
-                            meth(resource, **kwargs)))
+                attr_name = method[0]
+
+                def special_method(
+                    resource: Self = self,
+                    meth: Callable[..., RequestMethodResult] = method[1],
+                    **kwargs,
+                ) -> RequestMethodResult:
+                    return meth(resource, **kwargs)
+
+                if attr_name not in members:
+                    # This log message is useful for adding new stubs.
+                    logger.debug('%s is missing API stub for %s',
+                                 self.__class__.__name__, attr_name)
+                    setattr(self, attr_name, special_method)
+                elif is_api_stub(stub := getattr(self, attr_name)):
+                    replace_api_stub(self, attr_name, stub, special_method)
 
         # Generate request methods for any additional links the resource has.
         for link, body in self._links.items():
             if link not in SPECIAL_LINKS:
-                setattr(self,
-                        f'get_{link}',
-                        lambda resource=self, url=body['href'], **kwargs: (
-                            self._get_url(url, **kwargs)))
+                # Some resources have hyphens in the links.
+                attr_name = f'get_{link.replace("-", "_")}'
+                url = body['href']
+
+                def link_method(
+                    resource: Self = self,
+                    url: str = url,
+                    **kwargs,
+                ) -> RequestMethodResult:
+                    return resource._get_url(url, **kwargs)
+
+                if attr_name not in members:
+                    # This log message is useful for adding new stubs.
+                    logger.debug('%s is missing API stub for %s (%s)',
+                                 self.__class__.__name__, attr_name, url)
+                    setattr(self, attr_name, link_method)
+                elif is_api_stub(stub := getattr(self, attr_name)):
+                    replace_api_stub(self, attr_name, stub, link_method)
 
     def _wrap_field(
         self,
@@ -769,7 +802,36 @@ class Resource:
         """
         return self._payload
 
+    @api_stub
+    def get_self(
+        self,
+        *args,
+        **kwargs: QueryArgs,
+    ) -> Self:
+        """Get the resource's 'self ' link.
 
+        Args:
+            *args (tuple, unused):
+                Unused positional arguments.
+
+            **kwargs (dict):
+                Query arguments to include with the request.
+
+        Returns:
+            Resource:
+            The newly-fetched resource instance.
+
+        Raises:
+            rbtools.api.errors.APIError:
+                The Review Board API returned an error.
+
+            rbtools.api.errors.ServerInterfaceError:
+                An error occurred while communicating with the server.
+        """
+        raise NotImplementedError
+
+
+_TResource = TypeVar('_TResource', bound=Resource)
 _TResourceClass = TypeVar('_TResourceClass', bound=type[Resource])
 
 
@@ -793,6 +855,41 @@ def resource_mimetype(
 RequestMethodResult = Union[HttpRequest, Resource, None]
 
 
+class request_method_returns(Generic[_TResource]):
+    """Decorator to mark request methods with a specific return type.
+
+    When the return type of a method is known to be a specific type, this
+    decorator can be used in place of @request_method, and will rewrite the
+    annotation of the return value of the decorated method.
+
+    Version Added:
+        6.0
+    """
+
+    def __call__(
+        self,
+        f: Callable[_P, HttpRequest],
+    ) -> Callable[_P, _TResource]:
+        """Decorate the method.
+
+        Args:
+            f (callable):
+                The method to decorate.
+        """
+        @request_method
+        def wrapper(
+            self: Resource,
+            *args,
+            **kwargs,
+        ) -> HttpRequest:
+            return f(self, *args, **kwargs)
+
+        update_wrapper(wrapper, f)
+
+        return cast(Callable[_P, _TResource], wrapper)
+
+
+
 class ResourceDictField(MutableMapping[str, Any]):
     """Wrapper for dictionaries returned from a resource.
 
@@ -1018,7 +1115,7 @@ class ResourceDictField(MutableMapping[str, Any]):
             'parent resource.')
 
 
-class ResourceLinkField(ResourceDictField):
+class ResourceLinkField(ResourceDictField, Generic[_TResource]):
     """Wrapper for link dictionaries returned from a resource.
 
     In order to support operations on links found outside of a
@@ -1055,7 +1152,7 @@ class ResourceLinkField(ResourceDictField):
         super().__init__(resource, field_payload)
         self._transport = resource._transport
 
-    @request_method
+    @request_method_returns[_TResource]()
     def get(
         self,
         **query_args: QueryArgs,
@@ -1142,7 +1239,10 @@ class ResourceExtraDataField(ResourceDictField):
             f'{_EXTRA_DATA_DOCS_URL} for the format for these operations.')
 
 
-class ResourceListField(list[Any]):
+_TListValue = TypeVar('_TListValue')
+
+
+class ResourceListField(Generic[_TListValue], list[_TListValue]):
     """Wrapper for lists returned from a resource.
 
     Acts as a normal list, but wraps any returned items.
@@ -1161,7 +1261,7 @@ class ResourceListField(list[Any]):
     def __init__(
         self,
         resource: Resource,
-        list_field: list[Any],
+        list_field: list[_TListValue],
         item_mimetype: Optional[str] = None,
     ) -> None:
         """Initialize the field.
@@ -1184,7 +1284,7 @@ class ResourceListField(list[Any]):
     def __getitem__(
         self,
         key: int,
-    ) -> Any:
+    ) -> _TListValue:
         """Return the item at the given index.
 
         Args:
@@ -1200,7 +1300,7 @@ class ResourceListField(list[Any]):
         return self._resource._wrap_field(item,
                                           field_mimetype=self._item_mimetype)
 
-    def __iter__(self) -> Iterator[Any]:
+    def __iter__(self) -> Iterator[_TListValue]:
         """Iterate through the list.
 
         Yields:
@@ -1413,6 +1513,100 @@ class ItemResource(Resource):
                 f'payload={self._payload}, url={self._url}, '
                 f'token={self._token})')
 
+    @api_stub
+    def delete(
+        self,
+        *args,
+        **kwargs: QueryArgs,
+    ) -> None:
+        """Delete the resource.
+
+        Args:
+            *args (tuple, unused):
+                Unused positional arguments.
+
+            **kwargs (dict):
+                Query arguments to include with the request.
+
+        Raises:
+            rbtools.api.errors.APIError:
+                The Review Board API returned an error.
+
+            rbtools.api.errors.ServerInterfaceError:
+                An error occurred while communicating with the server.
+        """
+        raise NotImplementedError
+
+    @overload
+    def update(
+        self,
+        data: Optional[dict[str, Any]] = None,
+        query_args: Optional[dict[str, QueryArgs]] = None,
+        *args,
+        internal: Literal[False] = False,
+        **kwargs,
+    ) -> Self:
+        ...
+
+    @overload
+    def update(
+        self,
+        data: Optional[dict[str, Any]] = None,
+        query_args: Optional[dict[str, QueryArgs]] = None,
+        *args,
+        internal: Literal[True],
+        **kwargs,
+    ) -> HttpRequest:
+        ...
+
+    @api_stub
+    def update(
+        self,
+        data: Optional[dict[str, Any]] = None,
+        query_args: Optional[dict[str, QueryArgs]] = None,
+        *args,
+        **kwargs,
+    ) -> Union[Self, HttpRequest]:
+        """Update the resource.
+
+        Any ``extra_data_json`` (JSON Merge Patch) or ``extra_data_json_patch``
+        (JSON Patch) fields will be serialized to JSON and stored.
+
+        Any :samp:`extra_data__{key}` fields will be converted to
+        :samp:`extra_data.{key}` fields, which will be handled by the Review
+        Board API. These cannot store complex types.
+
+        Args:
+            resource (Resource):
+                The resource instance owning this create method.
+
+            data (dict, optional):
+                Data to send in the PUT request. This will be merged with
+                ``**kwargs``.
+
+            query_args (dict, optional):
+                Optional query arguments for the URL.
+
+            *args (tuple, unused):
+                Unused positional arguments.
+
+            **kwargs (dict):
+                Keyword arguments representing additional fields to set in the
+                request. This will be merged with ``data``.
+
+        Returns:
+            ItemResource:
+            The updated resource instance.
+
+        Raises:
+            rbtools.api.errors.APIError:
+                The Review Board API returned an error.
+
+            rbtools.api.errors.ServerInterfaceError:
+                An error occurred while communicating with the server.
+        """
+        raise NotImplementedError
+
 
 class CountResource(ItemResource):
     """Resource returned by a query with 'counts-only' true.
@@ -1471,7 +1665,10 @@ class CountResource(ItemResource):
         return HttpRequest(self._url, query_args=kwargs)
 
 
-class ListResource(Resource):
+TItemResource = TypeVar('TItemResource', bound=ItemResource)
+
+
+class ListResource(Generic[TItemResource], Resource):
     """The base class for List Resources.
 
     Any resource specific base classes for List Resources should
@@ -1584,7 +1781,7 @@ class ListResource(Resource):
     def __getitem__(
         self,
         index: int,
-    ) -> Any:
+    ) -> TItemResource:
         """Return the item at the specified index.
 
         Args:
@@ -1718,3 +1915,80 @@ class ListResource(Resource):
         return (f'{self.__class__.__name__}(transport={self._transport}, '
                 f'payload={self._payload}, url={self._url}, '
                 f'token={self._token}, item_mime_type={self._item_mime_type})')
+
+    @overload
+    def create(
+        self,
+        data: Optional[dict[str, Any]] = None,
+        query_args: Optional[dict[str, QueryArgs]] = None,
+        *args,
+        internal: Literal[False] = False,
+        **kwargs,
+    ) -> TItemResource:
+        ...
+
+    @overload
+    def create(
+        self,
+        data: Optional[dict[str, Any]] = None,
+        query_args: Optional[dict[str, QueryArgs]] = None,
+        *args,
+        internal: Literal[True],
+        **kwargs,
+    ) -> HttpRequest:
+        ...
+
+    @api_stub
+    def create(
+        self,
+        data: Optional[dict[str, Any]] = None,
+        query_args: Optional[dict[str, QueryArgs]] = None,
+        *args,
+        **kwargs,
+    ) -> Union[TItemResource, HttpRequest]:
+        """Create an item resource.
+
+        Any ``extra_data_json`` (JSON Merge Patch) or ``extra_data_json_patch``
+        (JSON Patch) fields will be serialized to JSON and stored.
+
+        Any :samp:`extra_data__{key}` fields will be converted to
+        :samp:`extra_data.{key}` fields, which will be handled by the Review
+        Board API. These cannot store complex types.
+
+        Args:
+            resource (Resource):
+                The resource instance owning this create method.
+
+            data (dict, optional):
+                Data to send in the POST request. This will be merged with
+                ``**kwargs``.
+
+            query_args (dict, optional):
+                Optional query arguments for the URL.
+
+            *args (tuple, unused):
+                Unused positional arguments.
+
+            **kwargs (dict):
+                Keyword arguments representing additional fields to set in the
+                request. This will be merged with ``data``.
+
+        Returns:
+            ItemResource:
+            The newly-created item resource.
+
+        Raises:
+            rbtools.api.errors.APIError:
+                The Review Board API returned an error.
+
+            rbtools.api.errors.ServerInterfaceError:
+                An error occurred while communicating with the server.
+        """
+        raise NotImplementedError
+
+
+#: Constants for text type fields.
+#:
+#: Version Added:
+#:     6.0
+TextType = Literal['plain', 'markdown', 'html']
diff --git a/rbtools/api/resource/diff.py b/rbtools/api/resource/diff.py
index 2552d3f565aa10229bd87d4e574eb5e08434beb5..bc87f337e76bad5693111b7bb4be6c7d8dea9f9a 100644
--- a/rbtools/api/resource/diff.py
+++ b/rbtools/api/resource/diff.py
@@ -81,7 +81,8 @@ class DiffItemResource(GetPatchMixin, ItemResource):
 
 
 @resource_mimetype('application/vnd.reviewboard.org.diffs')
-class DiffListResource(DiffUploaderMixin, ListResource):
+class DiffListResource(DiffUploaderMixin,
+                       ListResource[DiffItemResource]):
     """List resource for diffs."""
 
     @request_method
diff --git a/rbtools/api/resource/diff_commit.py b/rbtools/api/resource/diff_commit.py
index a8b446b952f99b22fa5dd98b93e3c1ebc6b65ee0..ff99e09be79713e46854e4fa924b6bd2b334a2d9 100644
--- a/rbtools/api/resource/diff_commit.py
+++ b/rbtools/api/resource/diff_commit.py
@@ -21,7 +21,8 @@ class DiffCommitItemResource(GetPatchMixin, ItemResource):
 
 
 @resource_mimetype('application/vnd.reviewboard.org.commits')
-class DiffCommitListResource(GetPatchMixin, ListResource):
+class DiffCommitListResource(GetPatchMixin,
+                             ListResource[DiffCommitItemResource]):
     """List resource for diff commits.
 
     Version Added:
diff --git a/rbtools/api/resource/diff_file_attachment.py b/rbtools/api/resource/diff_file_attachment.py
index e70a8900a5fb186cfab4d80f70983a6963aa263d..0a1bc28a28714b52433beff4453900bf21d5e965 100644
--- a/rbtools/api/resource/diff_file_attachment.py
+++ b/rbtools/api/resource/diff_file_attachment.py
@@ -30,7 +30,8 @@ class DiffFileAttachmentItemResource(ItemResource):
 
 
 @resource_mimetype('application/vnd.reviewboard.org.diff-file-attachments')
-class DiffFileAttachmentListResource(ListResource):
+class DiffFileAttachmentListResource(
+    ListResource[DiffFileAttachmentItemResource]):
     """List resource for diff file attachments.
 
     Version Added:
diff --git a/rbtools/api/resource/draft_diff_commit.py b/rbtools/api/resource/draft_diff_commit.py
index faf8289c42d8efced60880edec446f5c26a824f9..0cfc6ede48c767a6dd5692cb01542f64a55cd2c8 100644
--- a/rbtools/api/resource/draft_diff_commit.py
+++ b/rbtools/api/resource/draft_diff_commit.py
@@ -36,7 +36,7 @@ class DraftDiffCommitItemResource(GetPatchMixin, ItemResource):
 
 
 @resource_mimetype('application/vnd.reviewboard.org.draft-commits')
-class DraftDiffCommitListResource(ListResource):
+class DraftDiffCommitListResource(ListResource[DraftDiffCommitItemResource]):
     """List resource for draft diff commits."""
 
     @request_method
diff --git a/rbtools/api/resource/draft_file_attachment.py b/rbtools/api/resource/draft_file_attachment.py
index 40698ec5bd80d47d844ffedf839c346938181d9d..93e00146caea0e1d302df169e32babf0fbb60cdc 100644
--- a/rbtools/api/resource/draft_file_attachment.py
+++ b/rbtools/api/resource/draft_file_attachment.py
@@ -7,8 +7,12 @@ Version Added:
 
 from __future__ import annotations
 
-from rbtools.api.resource.base import ItemResource, resource_mimetype
-from rbtools.api.resource.file_attachment import FileAttachmentListResource
+from rbtools.api.resource.base import (
+    ItemResource,
+    ListResource,
+    resource_mimetype,
+)
+from rbtools.api.resource.mixins import AttachmentUploadMixin
 
 
 @resource_mimetype('application/vnd.reviewboard.org.draft-file-attachment')
@@ -21,5 +25,7 @@ class DraftFileAttachmentItemResource(ItemResource):
 
 
 @resource_mimetype('application/vnd.reviewboard.org.draft-file-attachments')
-class DraftFileAttachmentListResource(FileAttachmentListResource):
+class DraftFileAttachmentListResource(
+    AttachmentUploadMixin,
+    ListResource[DraftFileAttachmentItemResource]):
     """List resource for draft file attachments."""
diff --git a/rbtools/api/resource/draft_screenshot.py b/rbtools/api/resource/draft_screenshot.py
index cdaf68dc6d042f1e966bd3d79bb740736eba6842..d47a59dcab8a0ed4eeb71414c723639d500dbd89 100644
--- a/rbtools/api/resource/draft_screenshot.py
+++ b/rbtools/api/resource/draft_screenshot.py
@@ -7,8 +7,12 @@ Version Added:
 
 from __future__ import annotations
 
-from rbtools.api.resource.base import ItemResource, resource_mimetype
-from rbtools.api.resource.screenshot import ScreenshotListResource
+from rbtools.api.resource.base import (
+    ItemResource,
+    ListResource,
+    resource_mimetype,
+)
+from rbtools.api.resource.mixins import ScreenshotUploadMixin
 
 
 @resource_mimetype('application/vnd.reviewboard.org.draft-screenshot')
@@ -21,5 +25,6 @@ class DraftScreenshotItemResource(ItemResource):
 
 
 @resource_mimetype('application/vnd.reviewboard.org.draft-screenshots')
-class DraftScreenshotListResource(ScreenshotListResource):
+class DraftScreenshotListResource(ScreenshotUploadMixin,
+                                  ListResource[DraftScreenshotItemResource]):
     """List resource for draft screenshots."""
diff --git a/rbtools/api/resource/file_attachment.py b/rbtools/api/resource/file_attachment.py
index c4449f6c493fd4f8e7e6211e2dfdb5a878cfdd3e..a1d4708a729884ca92cfba84206d7ea9d9784fbe 100644
--- a/rbtools/api/resource/file_attachment.py
+++ b/rbtools/api/resource/file_attachment.py
@@ -7,18 +7,12 @@ Version Added:
 
 from __future__ import annotations
 
-from typing import Optional, TYPE_CHECKING
-
-from rbtools.api.request import HttpRequest
 from rbtools.api.resource.base import (
     ItemResource,
     ListResource,
-    request_method,
     resource_mimetype,
 )
-
-if TYPE_CHECKING:
-    from rbtools.api.request import QueryArgs
+from rbtools.api.resource.mixins import AttachmentUploadMixin
 
 
 @resource_mimetype('application/vnd.reviewboard.org.file-attachment')
@@ -32,48 +26,6 @@ class FileAttachmentItemResource(ItemResource):
 
 @resource_mimetype('application/vnd.reviewboard.org.file-attachments')
 @resource_mimetype('application/vnd.reviewboard.org.user-file-attachments')
-class FileAttachmentListResource(ListResource):
+class FileAttachmentListResource(AttachmentUploadMixin,
+                                 ListResource[FileAttachmentItemResource]):
     """List resource for file attachments."""
-
-    @request_method
-    def upload_attachment(
-        self,
-        filename: str,
-        content: bytes,
-        caption: Optional[str] = None,
-        attachment_history: Optional[str] = None,
-        **kwargs: QueryArgs,
-    ) -> HttpRequest:
-        """Upload a new attachment.
-
-        Args:
-            filename (str):
-                The name of the file.
-
-            content (bytes):
-                The content of the file to upload.
-
-            caption (str, optional):
-                The caption to set on the file attachment.
-
-            attachment_history (str, optional):
-                The ID of the FileAttachmentHistory to add this attachment to.
-
-            **kwargs (rbtools.api.request.QueryArgs):
-                Query arguments to include with the request.
-
-        Returns:
-            FileAttachmentItemResource:
-            The newly-created file attachment.
-        """
-        assert self._url is not None
-        request = HttpRequest(self._url, method='POST', query_args=kwargs)
-        request.add_file('path', filename, content)
-
-        if caption:
-            request.add_field('caption', caption)
-
-        if attachment_history:
-            request.add_field('attachment_history', attachment_history)
-
-        return request
diff --git a/rbtools/api/resource/file_diff.py b/rbtools/api/resource/file_diff.py
index 989e9b2282f9a4eb71f954d76c21909dc2debb21..74be28b526cd8ecc84ea23dd06f607723417ae4a 100644
--- a/rbtools/api/resource/file_diff.py
+++ b/rbtools/api/resource/file_diff.py
@@ -52,7 +52,7 @@ class FileDiffItemResource(GetPatchMixin, ItemResource):
 
 
 @resource_mimetype('application/vnd.reviewboard.org.files')
-class FileDiffListResource(ListResource):
+class FileDiffListResource(ListResource[FileDiffItemResource]):
     """List resource for file diffs.
 
     Version Added:
diff --git a/rbtools/api/resource/mixins.py b/rbtools/api/resource/mixins.py
index cb889536563160b608b8d0bb437477dae57daeb9..21908f4dc8452cb335221a6c6c7e1dee35e82d06 100644
--- a/rbtools/api/resource/mixins.py
+++ b/rbtools/api/resource/mixins.py
@@ -16,6 +16,59 @@ if TYPE_CHECKING:
     from rbtools.api.request import QueryArgs
 
 
+class AttachmentUploadMixin:
+    """A mixin for resources that implement an upload_attachment method.
+
+    Version Added:
+        6.0
+    """
+
+    _url: str
+
+    @request_method
+    def upload_attachment(
+        self,
+        filename: str,
+        content: bytes,
+        caption: Optional[str] = None,
+        attachment_history: Optional[str] = None,
+        **kwargs: QueryArgs,
+    ) -> HttpRequest:
+        """Upload a new attachment.
+
+        Args:
+            filename (str):
+                The name of the file.
+
+            content (bytes):
+                The content of the file to upload.
+
+            caption (str, optional):
+                The caption to set on the file attachment.
+
+            attachment_history (str, optional):
+                The ID of the FileAttachmentHistory to add this attachment to.
+
+            **kwargs (rbtools.api.request.QueryArgs):
+                Query arguments to include with the request.
+
+        Returns:
+            FileAttachmentItemResource:
+            The newly-created file attachment.
+        """
+        assert self._url is not None
+        request = HttpRequest(self._url, method='POST', query_args=kwargs)
+        request.add_file('path', filename, content)
+
+        if caption:
+            request.add_field('caption', caption)
+
+        if attachment_history:
+            request.add_field('attachment_history', attachment_history)
+
+        return request
+
+
 class DiffUploaderMixin:
     """A mixin for uploading diffs to a resource."""
 
@@ -97,3 +150,52 @@ class GetPatchMixin:
         return HttpRequest(self._url, query_args=kwargs, headers={
             'Accept': 'text/x-patch',
         })
+
+
+class ScreenshotUploadMixin:
+    """Mixin for resources that implement an upload_screenshot method.
+
+    Version Added:
+        6.0
+    """
+
+    _url: str
+
+    @request_method
+    def upload_screenshot(
+        self,
+        filename: str,
+        content: bytes,
+        caption: Optional[str] = None,
+        **kwargs: QueryArgs,
+    ) -> HttpRequest:
+        """Upload a new screenshot.
+
+        The content argument should contain the body of the screenshot
+        to be uploaded, in string format.
+
+        Args:
+            filename (str):
+                The filename of the screenshot.
+
+            content (bytes):
+                The image file content.
+
+            caption (str, optional):
+                The caption to add to the screenshot.
+
+            **kwargs (rbtools.api.request.QueryArgs):
+                Query arguments to include with the request.
+
+        Returns:
+            rbtools.api.resource.ScreenshotItemResource or
+            rbtools.api.resource.DraftScreenshotItemResource:
+            The newly-created screenshot.
+        """
+        request = HttpRequest(self._url, method='POST', query_args=kwargs)
+        request.add_file('path', filename, content)
+
+        if caption:
+            request.add_field('caption', caption)
+
+        return request
diff --git a/rbtools/api/resource/review_request.py b/rbtools/api/resource/review_request.py
index 07f2860844eb6150a0d1ec66c43fd924de7804aa..c93a2dce80991f22ad24bba3b7b564f50b7b412e 100644
--- a/rbtools/api/resource/review_request.py
+++ b/rbtools/api/resource/review_request.py
@@ -8,7 +8,7 @@ Version Added:
 from __future__ import annotations
 
 from collections import defaultdict, deque
-from typing import Optional, TYPE_CHECKING
+from typing import Literal, Optional, TYPE_CHECKING, Union, cast
 from urllib.parse import urljoin
 
 from typing_extensions import Self
@@ -16,13 +16,24 @@ from typing_extensions import Self
 from rbtools.api.resource.base import (
     ItemResource,
     ListResource,
+    api_stub,
     request_method,
+    request_method_returns,
     resource_mimetype,
 )
 from rbtools.utils.graphs import path_exists
 
 if TYPE_CHECKING:
     from rbtools.api.request import HttpRequest, QueryArgs
+    from rbtools.api.resource.base import (
+        ResourceExtraDataField,
+        ResourceLinkField,
+        ResourceListField,
+        TextType,
+    )
+    from rbtools.api.resource.diff import DiffItemResource, DiffListResource
+    from rbtools.api.resource.file_attachment import FileAttachmentListResource
+    from rbtools.api.resource.screenshot import ScreenshotListResource
 
 
 @resource_mimetype('application/vnd.reviewboard.org.review-request')
@@ -34,6 +45,121 @@ class ReviewRequestItemResource(ItemResource):
         Renamed from ReviewRequestResource.
     """
 
+    ######################
+    # Instance variables #
+    ######################
+
+    #: The reason why the review request was not approved.
+    approval_failure: Optional[str]
+
+    #: Whether the review request has been approved by reviewers.
+    #:
+    #: On a default install, a review request is approved if it has at least
+    #: one Ship It! and no open issues. Extensions may change these
+    #: requirements.
+    approved: bool
+
+    #: The list of review requests that this review request is blocking.
+    blocks: ResourceListField[ReviewRequestItemResource]
+
+    #: The branch that the code was changed on or will be committed to.
+    #:
+    #: This is a free-form field that can store any text.
+    branch: str
+
+    #: The list of bugs closed or referenced by this change.
+    bugs_closed: ResourceListField[str]
+
+    #: The change number that the review request represents.
+    #:
+    #: These are server-side repository-specific change numbers, and are not
+    #: supported by all types of repositories. This is deprecated in favor of
+    #: the ``commit_id`` field.
+    changenum: Optional[int]
+
+    #: The text describing the closing of the review request.
+    close_description: str
+
+    #: The current or forced text type for the ``close_description`` field.
+    close_description_text_type: TextType
+
+    #: The commit that the review request represents.
+    commit_id: str
+
+    #: Whether or not the review request was created with history support.
+    #:
+    #: A value of ``True`` indicates that the review request will have commits
+    #: attached.
+    created_with_history: bool
+
+    #: The list of review requests that this review request depends on.
+    depends_on: ResourceListField[ReviewRequestItemResource]
+
+    #: The review request's description.
+    description: str
+
+    #: The current or forced text type for the ``description`` field.
+    description_text_type: TextType
+
+    #: Extra data as part of the review request.
+    extra_data: ResourceExtraDataField
+
+    #: The numeric ID of the review request.
+    id: int
+
+    #: The number of dropped issues on this review request.
+    issue_dropped_count: int
+
+    #: The number of open issues on this review request.
+    issue_open_count: int
+
+    #: The number of resolved issues on this review request.
+    issue_resolved_count: int
+
+    #: The number of issues waiting for verification to resolve or drop.
+    issue_verifying_count: int
+
+    #: The date and time that the review request was last updated.
+    last_updated: str
+
+    #: The most recent diff.
+    latest_diff: Optional[ResourceLinkField[DiffItemResource]]
+
+    #: Whether or not the review request is currently visible to other users.
+    public: bool
+
+    #: The screenshots attached to the review request.
+    screenshots: ResourceLinkField[ScreenshotListResource]
+
+    #: The number of Ship It-s given to this review request.
+    ship_it_count: int
+
+    #: The current status of the review request.
+    status: Union[Literal['discarded'],
+                  Literal['pending'],
+                  Literal['submitted']]
+
+    #: The review request's brief summary.
+    summary: str
+
+    #: The list of review groups who were requested to review this change.
+    # TODO
+    # target_groups: ResourceListField[
+    #     ResourceLinkField[ReviewGroupItemResource]]
+
+    #: The list of users who were requested to review this change.
+    # TODO
+    # target_people: ResourceListField[ResourceLinkField[UserItemResource]]
+
+    #: The information on the testing that was done for the change.
+    testing_done: str
+
+    #: The current or forced text type for the ``testing_done`` field.
+    testing_done_text_type: TextType
+
+    #: The date and time that the review request was added.
+    time_added: str
+
     @property
     def absolute_url(self) -> str:
         """The absolute URL for the Review Request.
@@ -45,7 +171,7 @@ class ReviewRequestItemResource(ItemResource):
             str
         """
         if 'absolute_url' in self._fields:
-            return self._fields['absolute_url']
+            return cast(str, self._fields['absolute_url'])
         else:
             assert self._url is not None
 
@@ -65,9 +191,9 @@ class ReviewRequestItemResource(ItemResource):
         Type:
             str
         """
-        return self._fields.get('url', f'/r/{self.id}/')
+        return cast(str, self._fields.get('url', f'/r/{self.id}/'))
 
-    @request_method
+    @request_method_returns[Self]()
     def submit(
         self,
         description: Optional[str] = None,
@@ -99,7 +225,7 @@ class ReviewRequestItemResource(ItemResource):
 
         return self.update(data=data, internal=True)
 
-    @request_method
+    @request_method  # TODO TYPING
     def get_or_create_draft(
         self,
         **kwargs: QueryArgs,
@@ -191,9 +317,114 @@ class ReviewRequestItemResource(ItemResource):
 
         return graph
 
+    @api_stub
+    def get_diffs(
+        self,
+        **kwargs: QueryArgs,
+    ) -> DiffListResource:
+        """Get the diff list for the review request.
+
+        Args:
+            **kwargs (dict):
+                Query arguments to include with the request.
+
+        Returns:
+            rbtools.api.resource.DiffListResource:
+            The diff list resource.
+
+        Raises:
+            rbtools.api.errors.APIError:
+                The Review Board API returned an error.
+
+            rbtools.api.errors.ServerInterfaceError:
+                An error occurred while communicating with the server.
+        """
+        raise NotImplementedError
+
+    @api_stub
+    def get_file_attachments(
+        self,
+        **kwargs: QueryArgs,
+    ) -> FileAttachmentListResource:
+        """Get the file attachment list for the review request.
+
+        Args:
+            **kwargs (dict):
+                Query arguments to include with the request.
+
+        Returns:
+            rbtools.api.resource.FileAttachmentListResource:
+            The file attachment list resource.
+
+        Raises:
+            rbtools.api.errors.APIError:
+                The Review Board API returned an error.
+
+            rbtools.api.errors.ServerInterfaceError:
+                An error occurred while communicating with the server.
+        """
+        raise NotImplementedError
+
+    @api_stub
+    def get_latest_diff(
+        self,
+        **kwargs: QueryArgs,
+    ) -> DiffItemResource:
+        """Get the most recent diff list for the review request.
+
+        Args:
+            **kwargs (dict):
+                Query arguments to include with the request.
+
+        Returns:
+            rbtools.api.resource.DiffItemResource:
+            The diff item resource.
+
+        Raises:
+            rbtools.api.errors.APIError:
+                The Review Board API returned an error.
+
+            rbtools.api.errors.ServerInterfaceError:
+                An error occurred while communicating with the server.
+        """
+        raise NotImplementedError
+
+    @api_stub
+    def get_screenshots(
+        self,
+        **kwargs: QueryArgs,
+    ) -> ScreenshotListResource:
+        """Get the screenshot list for the review request.
+
+        Args:
+            **kwargs (dict):
+                Query arguments to include with the request.
+
+        Returns:
+            rbtools.api.resource.ScreenshotListResource:
+            The screenshot list resource.
+
+        Raises:
+            rbtools.api.errors.APIError:
+                The Review Board API returned an error.
+
+            rbtools.api.errors.ServerInterfaceError:
+                An error occurred while communicating with the server.
+        """
+        raise NotImplementedError
+
+    # TODO get_changes stub
+    # TODO get_diff_context stub
+    # TODO get_draft stub
+    # TODO get_last_update stub
+    # TODO get_repository stub
+    # TODO get_reviews stub
+    # TODO get_status_updates stub
+    # TODO get_submitter stub
+
 
 @resource_mimetype('application/vnd.reviewboard.org.review-requests')
-class ReviewRequestListResource(ListResource):
+class ReviewRequestListResource(ListResource[ReviewRequestItemResource]):
     """List resource for review requests.
 
     Version Added:
diff --git a/rbtools/api/resource/root.py b/rbtools/api/resource/root.py
index 526de158bc5306d26264ea42cd124a0975fc9220..1333bd01d37569b9bb929491ca946ca77bb2dae3 100644
--- a/rbtools/api/resource/root.py
+++ b/rbtools/api/resource/root.py
@@ -201,8 +201,7 @@ class RootResource(ItemResource):
         """Get a diff commit item resource.
 
         This method exists for compatibility with older versions of Review
-        Board which had broken URL names. :py:meth:`get_diff_commit` should be
-        used instead for modern servers.
+        Board. :py:meth:`get_diff_commit` should be used instead.
 
         Args:
             review_request_id (int):
@@ -824,32 +823,6 @@ class RootResource(ItemResource):
         """
         raise NotImplementedError
 
-    @api_stub
-    def get_self(
-        self,
-        **kwargs,
-    ) -> RootResource:
-        """Get the 'self' link.
-
-        This is functionally equivalent to :py:meth:`get_root`.
-
-        Args:
-            **kwargs (dict):
-                Query arguments to include with the request.
-
-        Returns:
-            RootResource:
-            The root resource.
-
-        Raises:
-            rbtools.api.errors.APIError:
-                The Review Board API returned an error.
-
-            rbtools.api.errors.ServerInterfaceError:
-                An error occurred while communicating with the server.
-        """
-        raise NotImplementedError
-
     @api_stub
     def get_screenshot(
         self,
diff --git a/rbtools/api/resource/screenshot.py b/rbtools/api/resource/screenshot.py
index a1b0758f39cccf93c42b3f9f1ccbb28b35175cba..3a73773f7a3cf83981080370c6dbcd2f608ea665 100644
--- a/rbtools/api/resource/screenshot.py
+++ b/rbtools/api/resource/screenshot.py
@@ -7,18 +7,12 @@ Version Added:
 
 from __future__ import annotations
 
-from typing import Optional, TYPE_CHECKING
-
-from rbtools.api.request import HttpRequest
 from rbtools.api.resource.base import (
     ItemResource,
     ListResource,
-    request_method,
     resource_mimetype,
 )
-
-if TYPE_CHECKING:
-    from rbtools.api.request import QueryArgs
+from rbtools.api.resource.mixins import ScreenshotUploadMixin
 
 
 @resource_mimetype('application/vnd.reviewboard.org.screenshot')
@@ -31,45 +25,6 @@ class ScreenshotItemResource(ItemResource):
 
 
 @resource_mimetype('application/vnd.reviewboard.org.screenshots')
-class ScreenshotListResource(ListResource):
+class ScreenshotListResource(ScreenshotUploadMixin,
+                             ListResource[ScreenshotItemResource]):
     """List resource for screenshots."""
-
-    @request_method
-    def upload_screenshot(
-        self,
-        filename: str,
-        content: bytes,
-        caption: Optional[str] = None,
-        **kwargs: QueryArgs,
-    ) -> HttpRequest:
-        """Upload a new screenshot.
-
-        The content argument should contain the body of the screenshot
-        to be uploaded, in string format.
-
-        Args:
-            filename (str):
-                The filename of the screenshot.
-
-            content (bytes):
-                The image file content.
-
-            caption (str, optional):
-                The caption to add to the screenshot.
-
-            **kwargs (rbtools.api.request.QueryArgs):
-                Query arguments to include with the request.
-
-        Returns:
-            ScreenshotItemResource:
-            The newly-created screenshot.
-        """
-        assert self._url is not None
-
-        request = HttpRequest(self._url, method='POST', query_args=kwargs)
-        request.add_file('path', filename, content)
-
-        if caption:
-            request.add_field('caption', caption)
-
-        return request
