diff --git a/djblets/webapi/core.py b/djblets/webapi/core.py
--- a/djblets/webapi/core.py
+++ b/djblets/webapi/core.py
@@ -30,7 +30,6 @@ from xml.sax.saxutils import XMLGenerator
 
 from django.conf import settings
 from django.contrib.auth.models import User, Group
-from django.db.models.query import QuerySet
 from django.core.serializers.json import DjangoJSONEncoder
 from django.http import HttpResponse, Http404
 from django.utils import simplejson
@@ -56,7 +55,7 @@ class WebAPIEncoder(object):
     )
     """
 
-    def encode(self, o):
+    def encode(self, o, *args, **kwargs):
         """
         Encodes an object.
 
@@ -67,35 +66,6 @@ class WebAPIEncoder(object):
         return None
 
 
-class BasicAPIEncoder(WebAPIEncoder):
-    """
-    A basic encoder that encodes dates, times, QuerySets, Users, and Groups.
-    """
-    def encode(self, o):
-        if isinstance(o, QuerySet):
-            return list(o)
-        elif isinstance(o, User):
-            return {
-                'id': o.id,
-                'username': o.username,
-                'first_name': o.first_name,
-                'last_name': o.last_name,
-                'fullname': o.get_full_name(),
-                'email': o.email,
-                'url': o.get_absolute_url(),
-            }
-        elif isinstance(o, Group):
-            return {
-                'id': o.id,
-                'name': o.name,
-            }
-        else:
-            try:
-                return DjangoJSONEncoder().default(o)
-            except TypeError:
-                return None
-
-
 class JSONEncoderAdapter(simplejson.JSONEncoder):
     """
     Adapts a WebAPIEncoder to be used with simplejson.
@@ -110,13 +80,18 @@ class JSONEncoderAdapter(simplejson.JSONEncoder):
         simplejson.JSONEncoder.__init__(self, *args, **kwargs)
         self.encoder = encoder
 
+    def encode(self, o, *args, **kwargs):
+        self.encode_args = args
+        self.encode_kwargs = kwargs
+        return super(JSONEncoderAdapter, self).encode(o)
+
     def default(self, o):
         """
         Encodes an object using the supplied WebAPIEncoder.
 
         If the encoder is unable to encode this object, a TypeError is raised.
         """
-        result = self.encoder.encode(o)
+        result = self.encoder.encode(o, *self.encode_args, **self.encode_kwargs)
 
         if result is None:
             raise TypeError("%r is not JSON serializable" % (o,))
@@ -134,7 +109,7 @@ class XMLEncoderAdapter(object):
     def __init__(self, encoder, *args, **kwargs):
         self.encoder = encoder
 
-    def encode(self, o):
+    def encode(self, o, *args, **kwargs):
         self.level = 0
         self.doIndent = False
 
@@ -142,25 +117,25 @@ class XMLEncoderAdapter(object):
         self.xml = XMLGenerator(stream, settings.DEFAULT_CHARSET)
         self.xml.startDocument()
         self.startElement("rsp")
-        self.__encode(o)
+        self.__encode(o, *args, **kwargs)
         self.endElement("rsp")
         self.xml.endDocument()
         self.xml = None
 
         return stream.getvalue()
 
-    def __encode(self, o):
+    def __encode(self, o, *args, **kwargs):
         if isinstance(o, dict):
             for key, value in o.iteritems():
                 self.startElement(key)
-                self.__encode(value)
+                self.__encode(value, *args, **kwargs)
                 self.endElement(key)
         elif isinstance(o, list):
             self.startElement("array")
 
             for i in o:
                 self.startElement("item")
-                self.__encode(i)
+                self.__encode(i, *args, **kwargs)
                 self.endElement("item")
 
             self.endElement("array")
@@ -176,12 +151,12 @@ class XMLEncoderAdapter(object):
         elif o is None:
             pass
         else:
-            result = self.encoder.encode(o)
+            result = self.encoder.encode(o, *args, **kwargs)
 
             if result is None:
                 raise TypeError("%r is not XML serializable" % (o,))
 
-            return self.__encode(result)
+            return self.__encode(result, *args, **kwargs)
 
     def startElement(self, name, attrs={}):
         self.addIndent()
@@ -209,7 +184,7 @@ class WebAPIResponse(HttpResponse):
     An API response, formatted for the desired file format.
     """
     def __init__(self, request, obj={}, stat='ok', api_format="json",
-                 status=200, headers={}):
+                 status=200, headers={}, encoders=[]):
         if api_format == "json":
             if request.FILES:
                 # When uploading a file using AJAX to a webapi view,
@@ -228,11 +203,13 @@ class WebAPIResponse(HttpResponse):
 
         super(WebAPIResponse, self).__init__(mimetype=mimetype,
                                              status=status)
+        self.request = request
         self.callback = request.GET.get('callback', None)
         self.api_data = {'stat': stat}
         self.api_data.update(obj)
         self.api_format = api_format
         self.content_set = False
+        self.encoders = encoders or get_registered_encoders()
 
         for header, value in headers.iteritems():
             self[header] = value
@@ -248,9 +225,12 @@ class WebAPIResponse(HttpResponse):
         the content is generated, but after the response is created.
         """
         class MultiEncoder(WebAPIEncoder):
-            def encode(self, o):
-                for encoder in get_registered_encoders():
-                    result = encoder.encode(o)
+            def __init__(self, encoders):
+                self.encoders = encoders
+
+            def encode(self, *args, **kwargs):
+                for encoder in self.encoders:
+                    result = encoder.encode(*args, **kwargs)
 
                     if result is not None:
                         return result
@@ -259,7 +239,7 @@ class WebAPIResponse(HttpResponse):
 
         if not self.content_set:
             adapter = None
-            encoder = MultiEncoder()
+            encoder = MultiEncoder(self.encoders)
 
             if self.api_format == "json":
                 adapter = JSONEncoderAdapter(encoder)
@@ -268,7 +248,8 @@ class WebAPIResponse(HttpResponse):
             else:
                 assert False
 
-            content = adapter.encode(self.api_data)
+            content = adapter.encode(self.api_data, api_format=self.api_format,
+                                     request=self.request)
 
             if self.callback != None:
                 content = "%s(%s);" % (self.callback, content)
@@ -294,10 +275,10 @@ class WebAPIResponsePaginated(WebAPIResponse):
     * max-results - The maximum number of results to return in the request.
     """
     def __init__(self, request, queryset, results_key="results",
-                 prev_key="prev_href", next_key="next_href",
+                 prev_key="prev", next_key="next",
                  total_results_key="total_results",
                  default_max_results=25, max_results_cap=200,
-                 *args, **kwargs):
+                 extra_data={}, *args, **kwargs):
         try:
             start = int(request.GET.get('start', 0))
         except ValueError:
@@ -317,15 +298,23 @@ class WebAPIResponsePaginated(WebAPIResponse):
             results_key: results,
             total_results_key: total_results,
         }
+        data.update(extra_data)
+
+        full_path = request.build_absolute_uri(request.path)
 
         if start > 0:
-            data[prev_key] = "%s?start=%s&max-results=%s" % \
-                             (request.path, max(start - max_results, 0),
-                              max_results)
+            data['links'][prev_key] = {
+                'method': 'GET',
+                'href': '%s?start=%s&max-results=%s' %
+                        (full_path, max(start - max_results, 0), max_results),
+            }
 
         if start + len(results) < total_results:
-            data[next_key] = "%s?start=%s&max-results=%s" % \
-                             (request.path, start + max_results, max_results)
+            data['links'][next_key] = {
+                'method': 'GET',
+                'href': '%s?start=%s&max-results=%s' %
+                        (full_path, start + max_results, max_results),
+            }
 
         WebAPIResponse.__init__(self, request, obj=data, *args, **kwargs)
 
@@ -335,7 +324,8 @@ class WebAPIResponseError(WebAPIResponse):
     A general error response, containing an error code and a human-readable
     message.
     """
-    def __init__(self, request, err, extra_params={}, *args, **kwargs):
+    def __init__(self, request, err, extra_params={}, headers={},
+                 *args, **kwargs):
         errdata = {
             'err': {
                 'code': err.code,
@@ -344,8 +334,11 @@ class WebAPIResponseError(WebAPIResponse):
         }
         errdata.update(extra_params)
 
+        headers = headers.copy()
+        headers.update(err.headers)
+
         WebAPIResponse.__init__(self, request, obj=errdata, stat="fail",
-                                status=err.http_status, headers=err.headers,
+                                status=err.http_status, headers=headers,
                                 *args, **kwargs)
 
 
@@ -393,3 +386,10 @@ def get_registered_encoders():
             __registered_encoders.append(encoder_class())
 
     return __registered_encoders
+
+
+# Backwards-compatibility
+#
+# This must be done after the classes in order to avoid a
+# circular import problem.
+from djblets.webapi.encoders import BasicAPIEncoder
diff --git a/djblets/webapi/decorators.py b/djblets/webapi/decorators.py
--- a/djblets/webapi/decorators.py
+++ b/djblets/webapi/decorators.py
@@ -25,9 +25,24 @@
 #
 
 
+from django.http import HttpRequest
+
 from djblets.util.decorators import simple_decorator
 from djblets.webapi.core import WebAPIResponse, WebAPIResponseError
-from djblets.webapi.errors import NOT_LOGGED_IN, PERMISSION_DENIED
+from djblets.webapi.errors import NOT_LOGGED_IN, PERMISSION_DENIED, \
+                                  INVALID_FORM_DATA
+
+
+def _find_httprequest(args):
+    if isinstance(args[0], HttpRequest):
+        request = args[0]
+    else:
+        # This should be in a class then.
+        assert len(args) > 1
+        request = args[1]
+        assert isinstance(request, HttpRequest)
+
+    return request
 
 
 @simple_decorator
@@ -36,11 +51,11 @@ def webapi(view_func):
     Checks the API format desired for this handler and sets it in the
     resulting WebAPIResponse.
     """
-    def _dec(request, api_format="json", *args, **kwargs):
-        response = view_func(request, *args, **kwargs)
+    def _dec(*args, **kwargs):
+        response = view_func(*args, **kwargs)
 
         if isinstance(response, WebAPIResponse):
-            response.api_format = api_format
+            response.api_format = kwargs.get('api_format', 'json')
 
         return response
 
@@ -55,21 +70,23 @@ def webapi_login_required(view_func):
     is not logged in, a NOT_LOGGED_IN error (HTTP 401 Unauthorized) is
     returned.
     """
-    def _checklogin(request, api_format="json", *args, **kwargs):
+    def _checklogin(*args, **kwargs):
         from djblets.webapi.auth import basic_access_login
 
+        request = _find_httprequest(args)
+
         if not request.user.is_authenticated():
             # See if the request contains authentication tokens
             if 'HTTP_AUTHORIZATION' in request.META:
                 basic_access_login(request)
 
         if request.user.is_authenticated():
-            response = view_func(request, *args, **kwargs)
+            response = view_func(*args, **kwargs)
         else:
             response = WebAPIResponseError(request, NOT_LOGGED_IN)
 
         if isinstance(response, WebAPIResponse):
-            response.api_format = api_format
+            response.api_format = kwargs.get('api_format', 'json')
 
         return response
 
@@ -84,19 +101,128 @@ def webapi_permission_required(perm):
     does not have the proper permissions.
     """
     def _dec(view_func):
-        def _checkpermissions(request, api_format="json", *args, **kwargs):
+        def _checkpermissions(*args, **kwargs):
+            request = _find_httprequest(args)
+
             if not request.user.is_authenticated():
                 response = WebAPIResponseError(request, NOT_LOGGED_IN)
             elif not request.user.has_perm(perm):
                 response = WebAPIResponseError(request, PERMISSION_DENIED)
             else:
-                response = view_func(request, *args, **kwargs)
+                response = view_func(*args, **kwargs)
 
             if isinstance(response, WebAPIResponse):
-                response.api_format = api_format
+                response.api_format = kwargs.get('api_format', 'json')
 
             return response
 
         return _checkpermissions
 
     return _dec
+
+
+def webapi_request_fields(required={}, optional={}, allow_unknown=False):
+    """Validates incoming fields for a request.
+
+    This is a helpful decorator for ensuring that the fields in the request
+    match what the caller expects.
+
+    If any field is set in the request that is not in either ``required``
+    or ``optional`` and ``allow_unknown`` is True, the response will be an
+    INVALID_FORM_DATA error. The exceptions are the special fields
+    ``method`` and ``callback``.
+
+    If any field in ``required`` is not passed in the request, these will
+    also be listed in the INVALID_FORM_DATA response.
+
+    The ``required`` and ``optional`` parameters are dictionaries
+    mapping field name to an info dictionary, which contains the following
+    keys:
+
+      * ``type`` - The data type for the field.
+      * ```description`` - A description of the field.
+
+    For example:
+
+        @webapi_request_fields(required={
+            'name': {
+                'type': str,
+                'description': 'The name of the object',
+            }
+        })
+    """
+    def _dec(view_func):
+        def _validate(*args, **kwargs):
+            request = _find_httprequest(args)
+
+            if request.method == 'GET':
+                request_fields = request.GET
+            else:
+                request_fields = request.POST
+
+            invalid_fields = {}
+            supported_fields = required.copy()
+            supported_fields.update(optional)
+
+            if not allow_unknown:
+                for field_name in request_fields:
+                    if field_name in ('_method', 'callback'):
+                        # These are special names and can be ignored.
+                        continue
+
+                    if field_name not in supported_fields:
+                        invalid_fields[field_name] = ['Field is not supported']
+
+            for field_name, info in required.iteritems():
+                temp_fields = request_fields
+
+                if info['type'] == file:
+                    temp_fields = request.FILES
+
+                if temp_fields.get(field_name, None) is None:
+                    invalid_fields[field_name] = ['This field is required']
+
+            new_kwargs = kwargs.copy()
+
+            for field_name, info in supported_fields.iteritems():
+                if isinstance(info['type'], file):
+                    continue
+
+                value = request_fields.get(field_name, None)
+
+                if value is not None:
+                    if type(info['type']) in (list, tuple):
+                        # This is a multiple-choice. Make sure the value is
+                        # valid.
+                        choices = info['type']
+
+                        if value not in choices:
+                            invalid_fields[field_name] = [
+                                "'%s' is not a valid value. Valid values "
+                                "are: %s" % (
+                                    value,
+                                    ', '.join(["'%s'" for choice in choices])
+                                )
+                            ]
+                    elif issubclass(info['type'], bool):
+                        value = value in (1, "1", True, "True")
+                    elif issubclass(info['type'], int):
+                        try:
+                            value = int(value)
+                        except ValueError:
+                            invalid_fields[field_name] = [
+                                "'%s' is not an integer" % value
+                            ]
+
+                new_kwargs[field_name] = value
+
+            if invalid_fields:
+                return INVALID_FORM_DATA, {
+                    'fields': invalid_fields,
+                }
+
+            return view_func(*args, **new_kwargs)
+
+        return _validate
+
+    return _dec
diff --git a/djblets/webapi/encoders.py b/djblets/webapi/encoders.py
--- /dev/null
+++ b/djblets/webapi/encoders.py
@@ -0,0 +1,34 @@
+from django.contrib.auth.models import User, Group
+from django.core.serializers.json import DjangoJSONEncoder
+from django.db.models.query import QuerySet
+
+from djblets.webapi.core import WebAPIEncoder
+
+
+class BasicAPIEncoder(WebAPIEncoder):
+    """
+    A basic encoder that encodes dates, times, QuerySets, Users, and Groups.
+    """
+    def encode(self, o, *args, **kwargs):
+        if isinstance(o, QuerySet):
+            return list(o)
+        elif isinstance(o, User):
+            return {
+                'id': o.id,
+                'username': o.username,
+                'first_name': o.first_name,
+                'last_name': o.last_name,
+                'fullname': o.get_full_name(),
+                'email': o.email,
+                'url': o.get_absolute_url(),
+            }
+        elif isinstance(o, Group):
+            return {
+                'id': o.id,
+                'name': o.name,
+            }
+        else:
+            try:
+                return DjangoJSONEncoder().default(o)
+            except TypeError:
+                return None
diff --git a/djblets/webapi/resources.py b/djblets/webapi/resources.py
--- /dev/null
+++ b/djblets/webapi/resources.py
@@ -0,0 +1,772 @@
+from django.conf.urls.defaults import include, patterns, url
+from django.contrib.auth.models import User, Group
+from django.core.urlresolvers import reverse, NoReverseMatch
+from django.db import models
+from django.http import HttpResponseNotAllowed, HttpResponse
+
+from djblets.util.misc import never_cache_patterns
+from djblets.webapi.core import WebAPIResponse, WebAPIResponseError, \
+                                WebAPIResponsePaginated
+from djblets.webapi.decorators import webapi_login_required
+from djblets.webapi.errors import WebAPIError, DOES_NOT_EXIST, \
+                                  PERMISSION_DENIED
+
+
+class WebAPIResource(object):
+    """A resource living at a specific URL, representing an object or list
+    of objects.
+
+    A WebAPIResource is a RESTful resource living at a specific URL. It
+    can represent either an object or a list of objects, and can respond
+    to various HTTP methods (GET, POST, PUT, DELETE).
+
+    Subclasses are expected to override functions and variables in order to
+    provide specific functionality, such as modifying the resource or
+    creating a new resource.
+
+
+    Representing Models
+    -------------------
+
+    Most resources will have ``model`` set to a Model subclass, and
+    ``fields`` set to list the fields that would be shown when
+
+    Each resource will also include a ``link`` dictionary that maps
+    a key (resource name or action) to a dictionary containing the URL
+    (``href``) and the HTTP method that's to be used for that URL
+    (``method``). This will include a special ``self`` key that links to
+    that resource's actual location.
+
+    An example of this might be::
+
+       'links': {
+           'self': {
+               'method': 'GET',
+               'href': '/path/to/this/resource/'
+           },
+           'update': {
+               'method': 'PUT',
+               'href': '/path/to/this/resource/'
+           }
+       }
+
+    Resources associated with a model may want to override the ``get_queryset``
+    function to return a queryset with a more specific query.
+
+    By default, an individual object's key name in the resulting payloads
+    will be set to the lowercase class name of the object, and the plural
+    version used for lists will be the same but with 's' appended to it. This
+    can be overridden by setting ``name`` and ``name_plural``.
+
+
+    Matching Objects
+    ----------------
+
+    Objects are generally queried by their numeric object ID and mapping that
+    to the object's ``pk`` attribute. For this to work, the ``uri_object_key``
+    attribute must be set to the name in the regex for the URL that will
+    be captured and passed to the handlers for this resource. The
+    ``uri_object_key_regex`` attribute can be overridden to specify the
+    regex for matching this ID (useful for capturing names instead of
+    numeric IDs) and ``model_object_key`` can be overridden to specify the
+    model field that will be matched against.
+
+
+    Parents and URLs
+    ----------------
+
+    Resources typically have a parent resource, of which the resource is
+    a subclass. Resources will often list their children (by setting
+    ``list_child_resources`` and ``item_child_resources`` in a subclass
+    to lists of other WebAPIResource instances). This makes the entire tree
+    navigatable. The URLs are built up automatically, so long as the result
+    of get_url_patterns() from top-level resources are added to the Django
+    url_patterns variables commonly found in urls.py.
+
+    Child objects should set the ``model_parent_key`` variable to the
+    field name of the object's parent in the resource hierarchy. This
+    allows WebAPIResource to build a URL with the right values filled in in
+    order to make a URL to this object.
+
+    If the parent is dynamic based on certain conditions, then the
+    ``get_parent_object`` function can be overridden instead.
+
+
+    Object Serialization
+    --------------------
+
+    Objects are serialized through the ``serialize_object`` function.
+    This rarely needs to be overridden, but can be called from WebAPIEncoders
+    in order to serialize the object. By default, this will loop through
+    the ``fields`` variable and add each value to the resulting dictionary.
+
+    Values can be specially serialized by creating functions in the form of
+    ``serialize_<fieldname>_field``. These functions take the object being
+    serialized and must return a value that can be fed to the encoder.
+
+
+    Handling Requests
+    -----------------
+
+    WebAPIResource calls the following functions based on the type of
+    HTTP request:
+
+      * ``get`` - HTTP GET for individual objects.
+      * ``get_list`` - HTTP GET for resources representing lists of objects.
+      * ``create`` - HTTP POST on resources representing lists of objects.
+                     This is expected to return the object and an HTTP
+                     status of 201 CREATED, on success.
+      * ``update`` - HTTP PUT on individual objects to modify their state
+                     based on full or partial data.
+      * ``delete`` - HTTP DELETE on an individual object. This is expected
+                     to return a status of HTTP 204 No Content on success.
+                     The default implementation just deletes the object.
+
+    Any function that is not implemented will return an HTTP 405 Method
+    Not Allowed. Functions that have handlers provided should set
+    ``allowed_methods`` to a tuple of the HTTP methods allowed. For example::
+
+        allowed_methods = ('GET', POST', 'DELETE')
+
+    These functions are passed an HTTPRequest and a list of arguments
+    captured in the URL and are expected to return standard HTTP response
+    codes, along with a payload in most cases. The functions can return any of:
+
+      * A HttpResponse
+      * A WebAPIResponse
+      * A WebAPIError
+      * A tuple of (WebAPIError, Payload)
+      * A tuple of (WebAPIError, Payload Dictionary, Headers Dictionary)
+      * A tuple of (HTTP status, Payload)
+      * A tuple of (HTTP status, Payload Dictionary, Headers Dictionary)
+
+    In general, it's best to return one of the tuples containing an HTTP
+    status, and not any object, but there are cases where an object is
+    necessary.
+
+    Commonly, a handler will need to fetch parent objects in order to make
+    some request. The values for all captured object IDs in the URL are passed
+    to the handler, but it's best to not use these directly. Instead, the
+    handler should accept a **kwargs parameter, and then call the parent
+    resource's ``get_object`` function and pass in that **kwargs. For example::
+
+      def create(self, request, *args, **kwargs):
+          try:
+              my_parent = myParentResource.get_object(request, *args, **kwargs)
+          except ObjectDoesNotExist:
+              return DOES_NOT_EXIST
+
+
+    Faking HTTP Methods
+    -------------------
+
+    There are clients that can't actually request anything but HTTP POST
+    and HTTP GET. An HTML form is one such example, and Flash applications
+    are another. For these cases, an HTTP POST can be made, with a special
+    ``_method`` parameter passed to the URL. This can be set to the HTTP
+    method that's desired. For example, ``PUT`` or ``DELETE``.
+
+
+    Permissions
+    -----------
+
+    Unless overridden, an object cannot be modified, created, or deleted
+    if the user is not logged in and if an appropriate permission function
+    does not return True. These permission functions are:
+
+    * ``has_access_permissions`` - Used for HTTP GET calls. Returns True
+                                   by default.
+    * ``has_modify_permissions`` - Used for HTTP POST or PUT calls, if
+                                   called by the subclass. Returns False
+                                   by default.
+    * ``has_delete_permissions`` - Used for HTTP DELETE permissions. Returns
+                                   False by default.
+    """
+
+    # Configuration
+    model = None
+    fields = ()
+    uri_object_key_regex = '[0-9]+'
+    uri_object_key = None
+    model_object_key = 'pk'
+    model_parent_key = None
+    list_child_resources = []
+    item_child_resources = []
+    allowed_methods = ('GET',)
+
+    # State
+    method_mapping = {
+        'GET': 'get',
+        'POST': 'post',
+        'PUT': 'put',
+        'DELETE': 'delete',
+    }
+
+    _parent_resource = None
+
+    def __call__(self, request, api_format="json", *args, **kwargs):
+        """Invokes the correct HTTP handler based on the type of request."""
+        method = request.method
+
+        if method == 'POST':
+            # Not all clients can do anything other than GET or POST.
+            # So, in the case of POST, we allow overriding the method
+            # used.
+            method = request.POST.get('_method', kwargs.get('_method', method))
+        elif method == 'PUT':
+            # Normalize the PUT data so we can get to it.
+            # This is due to Django's treatment of PUT vs. POST. They claim
+            # that PUT, unlike POST, is not necessarily represented as form
+            # data, so they do not parse it. However, that gives us no clean way
+            # of accessing the data. So we pretend it's POST for a second in
+            # order to parse.
+            #
+            # This must be done only for legitimate PUT requests, not faked
+            # ones using ?method=PUT.
+            try:
+                request.method = 'POST'
+                request._load_post_and_files()
+                request.method = 'PUT'
+            except AttributeError:
+                request.META['REQUEST_METHOD'] = 'POST'
+                request._load_post_and_files()
+                request.META['REQUEST_METHOD'] = 'PUT'
+
+        request.PUT = request.POST
+
+
+        if method in self.allowed_methods:
+            if (method == "GET" and
+                ((self.uri_object_key is not None and
+                  self.uri_object_key not in kwargs) or
+                 (self.uri_object_key is None and
+                  self.list_child_resources))):
+                view = self.get_list
+            else:
+                view = getattr(self, self.method_mapping.get(method, None))
+        else:
+            view = None
+
+        if view and callable(view):
+            result = view(request, api_format=api_format, *args, **kwargs)
+
+            if isinstance(result, WebAPIResponse):
+                return result
+            elif isinstance(result, WebAPIError):
+                return WebAPIResponseError(request, err=result,
+                                           api_format=api_format)
+            elif isinstance(result, tuple):
+                headers = {}
+
+                if len(result) == 3:
+                    headers = result[2]
+
+                if isinstance(result[0], WebAPIError):
+                    return WebAPIResponseError(request,
+                                               err=result[0],
+                                               headers=headers,
+                                               extra_params=result[1],
+                                               api_format=api_format)
+                else:
+                    return WebAPIResponse(request,
+                                          status=result[0],
+                                          obj=result[1],
+                                          headers=headers,
+                                          api_format=api_format)
+            elif isinstance(result, HttpResponse):
+                return result
+            else:
+                raise AssertionError(result)
+        else:
+            return HttpResponseNotAllowed(self.allowed_methods)
+
+    @property
+    def __name__(self):
+        return self.__class__.__name__
+
+    @property
+    def name(self):
+        """Returns the name of the object, used for keys in the payloads."""
+        if self.model:
+            return self.model.__name__.lower()
+        else:
+            return self.__name__.lower()
+
+    @property
+    def name_plural(self):
+        """Returns the plural name of the object, used for lists."""
+        return self.name + 's'
+
+    @property
+    def item_result_key(self):
+        """Returns the key for single objects in the payload."""
+        return self.name
+
+    @property
+    def list_result_key(self):
+        """Returns the key for lists of objects in the payload."""
+        return self.name_plural
+
+    @property
+    def uri_name(self):
+        """Returns the name of the resource in the URI.
+
+        This can be overridden when the name in the URI needs to differ
+        from the name used for the resource.
+        """
+        return self.name_plural.replace('_', '-')
+
+    def get_object(self, request, *args, **kwargs):
+        """Returns an object, given captured parameters from a URL.
+
+        This will perform a query for the object, taking into account
+        ``model_object_key``, ``uri_object_key``, and any captured parameters
+        from the URL.
+
+        This requires that ``model`` and ``uri_object_key`` be set.
+        """
+        assert self.model
+        assert self.uri_object_key
+
+        queryset = self.get_queryset(request, *args, **kwargs)
+
+        return queryset.get(**{
+            self.model_object_key: kwargs[self.uri_object_key]
+        })
+
+    def post(self, *args, **kwargs):
+        """Handles HTTP POSTs.
+
+        This is not meant to be overridden unless there are specific needs.
+
+        This will invoke ``create`` if doing an HTTP POST on a list resource.
+
+        By default, an HTTP POST is not allowed on individual object
+        resourcces.
+        """
+
+        if 'POST' not in self.allowed_methods:
+            return HttpResponseNotAllowed(self.allowed_methods)
+
+        if (self.uri_object_key is None or
+            kwargs.get(self.uri_object_key, None) is None):
+            return self.create(*args, **kwargs)
+
+        # Don't allow POSTs on children by default.
+        allowed_methods = list(self.allowed_methods)
+        allowed_methods.remove('POST')
+
+        return HttpResponseNotAllowed(allowed_methods)
+
+    def put(self, request, *args, **kwargs):
+        """Handles HTTP PUTs.
+
+        This is not meant to be overridden unless there are specific needs.
+
+        This will just invoke ``update``.
+        """
+        return self.update(request, *args, **kwargs)
+
+    def get(self, request, *args, **kwargs):
+        """Handles HTTP GETs to individual object resources.
+
+        By default, this will check for access permissions and query for
+        the object. It will then return a serialized form of the object.
+
+        This may need to be overridden if needing more complex logic.
+        """
+        if not self.model or self.uri_object_key is None:
+            return HttpResponseNotAllowed(self.allowed_methods)
+
+        try:
+            obj = self.get_object(request, *args, **kwargs)
+        except self.model.DoesNotExist:
+            return DOES_NOT_EXIST
+
+        if not self.has_access_permissions(request, obj, *args, **kwargs):
+            return PERMISSION_DENIED
+
+        return 200, {
+            self.item_result_key: self.serialize_object(obj, request=request,
+                                                        *args, **kwargs),
+        }
+
+    def get_list(self, request, *args, **kwargs):
+        """Handles HTTP GETs to list resources.
+
+        By default, this will query for a list of objects and return the
+        list in a serialized form.
+        """
+        data = {
+            'links': self.get_links(self.list_child_resources,
+                                    request=request, *args, **kwargs),
+        }
+
+        if self.model:
+            return WebAPIResponsePaginated(
+                request,
+                queryset=self.get_queryset(request, is_list=True,
+                                           *args, **kwargs),
+                results_key=self.list_result_key,
+                extra_data=data)
+        else:
+            return 200, data
+
+    @webapi_login_required
+    def create(self, request, api_format, *args, **kwargs):
+        """Handles HTTP POST requests to list resources.
+
+        This is used to create a new object on the list, given the
+        data provided in the request. It should usually return
+        HTTP 201 Created upon success.
+
+        By default, this returns HTTP 405 Method Not Allowed.
+        """
+        return HttpResponseNotAllowed(self.allowed_methods)
+
+    @webapi_login_required
+    def update(self, request, api_format, *args, **kwargs):
+        """Handles HTTP PUT requests to object resources.
+
+        This is used to update an object, given full or partial data provided
+        in the request. It should usually return HTTP 200 OK upon success.
+
+        By default, this returns HTTP 405 Method Not Allowed.
+        """
+        return HttpResponseNotAllowed(self.allowed_methods)
+
+    @webapi_login_required
+    def delete(self, request, api_format, *args, **kwargs):
+        """Handles HTTP DELETE requests to object resources.
+
+        This is used to delete an object, if the user has permissions to
+        do so.
+
+        By default, this deletes the object and returns HTTP 204 No Content.
+        """
+        if not self.model or self.uri_object_key is None:
+            return HttpResponseNotAllowed(self.allowed_methods)
+
+        try:
+            queryset = self.get_queryset(request, *args, **kwargs)
+            obj = queryset.get(**{
+                self.model_object_key: kwargs[self.uri_object_key]
+            })
+        except self.model.DoesNotExist:
+            return DOES_NOT_EXIST
+
+        if not self.has_delete_permissions(request, obj, *args, **kwargs):
+            return PERMISSION_DENIED
+
+        obj.delete()
+
+        return 204, {}
+
+    def get_queryset(self, request, is_list=False, *args, **kwargs):
+        """Returns a queryset used for querying objects or lists of objects.
+
+        This can be overridden to filter the object list, such as for hiding
+        non-public objects.
+
+        The ``is_list`` parameter can be used to specialize the query based
+        on whether an individual object or a list of objects is being queried.
+        """
+        return self.model.objects.all()
+
+    def get_url_patterns(self):
+        """Returns the Django URL patterns for this object and its children.
+
+        This is used to automatically build up the URL hierarchy for all
+        objects. Projects should call this for top-level resources and
+        return them in the ``urls.py`` files.
+        """
+        urlpatterns = never_cache_patterns('',
+            url(r'^$', self, name=self._build_named_url(self.name_plural)),
+        )
+
+        for resource in self.list_child_resources:
+            resource._parent_resource = self
+            child_regex = r'^' + resource.uri_name + '/'
+            urlpatterns += patterns('',
+                url(child_regex, include(resource.get_url_patterns())),
+            )
+
+        if self.uri_object_key:
+            # If the resource has particular items in it...
+            base_regex = r'^(?P<%s>%s)/' % (self.uri_object_key,
+                                            self.uri_object_key_regex)
+
+            urlpatterns += never_cache_patterns('',
+                url(base_regex + '$', self,
+                    name=self._build_named_url(self.name))
+            )
+
+            for resource in self.item_child_resources:
+                resource._parent_resource = self
+                child_regex = base_regex + resource.uri_name + '/'
+                urlpatterns += patterns('',
+                    url(child_regex, include(resource.get_url_patterns())),
+                )
+
+        return urlpatterns
+
+    def has_access_permissions(self, request, obj, *args, **kwargs):
+        """Returns whether or not the user has read access to this object."""
+        return True
+
+    def has_modify_permissions(self, request, obj, *args, **kwargs):
+        """Returns whether or not the user can modify this object."""
+        return False
+
+    def has_delete_permissions(self, request, obj, *args, **kwargs):
+        """Returns whether or not the user can delete this object."""
+        return False
+
+    def serialize_object(self, obj, *args, **kwargs):
+        """Serializes the object into a Python dictionary."""
+        data = {}
+
+        for field in self.fields:
+            serialize_func = getattr(self, "serialize_%s_field" % field, None)
+
+            if serialize_func and callable(serialize_func):
+                value = serialize_func(obj)
+            else:
+                value = getattr(obj, field)
+
+                if isinstance(value, models.Manager):
+                    value = value.all()
+                elif isinstance(value, models.ForeignKey):
+                    value = value.get()
+
+            data[field] = value
+
+        data['links'] = \
+            self.get_links(self.item_child_resources, obj, *args, **kwargs)
+
+        return data
+
+    def get_links(self, resources=[], obj=None, request=None,
+                  *args, **kwargs):
+        """Returns a dictionary of links coming off this resource.
+
+        The resulting links will point to the resources passed in
+        ``resources``, and will also provide special resources for
+        ``self`` (which points back to the official location for this
+        resource) and one per HTTP method/operation allowed on this
+        resource.
+        """
+        links = {}
+        base_href = None
+
+        if obj:
+            base_href = self.get_href(obj, request, *args, **kwargs)
+
+        if not base_href:
+            # We may have received None from the URL above.
+            if request:
+                base_href = request.build_absolute_uri()
+            else:
+                base_href = ''
+
+        links['self'] = {
+            'method': 'GET',
+            'href': base_href,
+        }
+
+        # base_href without any query arguments.
+        i = base_href.find('?')
+
+        if i != -1:
+            clean_base_href = base_href[:i]
+        else:
+            clean_base_href = base_href
+
+        if 'POST' in self.allowed_methods and not obj:
+            links['create'] = {
+                'method': 'POST',
+                'href': clean_base_href,
+            }
+
+        if 'PUT' in self.allowed_methods and obj:
+            links['update'] = {
+                'method': 'PUT',
+                'href': clean_base_href,
+            }
+
+        if 'DELETE' in self.allowed_methods and obj:
+            links['delete'] = {
+                'method': 'DELETE',
+                'href': clean_base_href,
+            }
+
+        for resource in resources:
+            links[resource.name_plural] = {
+                'method': 'GET',
+                'href': '%s%s/' % (clean_base_href, resource.uri_name),
+            }
+
+        return links
+
+    def get_href(self, obj, request, *args, **kwargs):
+        """Returns the URL for this object."""
+        if not self.uri_object_key:
+            return None
+
+        href_kwargs = {
+            self.uri_object_key: getattr(obj, self.model_object_key),
+        }
+        href_kwargs.update(self.get_href_parent_ids(obj))
+
+        try:
+            return request.build_absolute_uri(
+                reverse(self._build_named_url(self.name),
+                        kwargs=href_kwargs))
+        except NoReverseMatch:
+            return None
+
+    def get_href_parent_ids(self, obj):
+        """Returns a dictionary mapping parent object keys to their values for
+        an object.
+        """
+        parent_ids = {}
+
+        if self._parent_resource and self.model_parent_key:
+            assert self._parent_resource.uri_object_key
+
+            parent_obj = self.get_parent_object(obj)
+            parent_ids = self._parent_resource.get_href_parent_ids(parent_obj)
+            parent_ids[self._parent_resource.uri_object_key] = \
+                getattr(parent_obj, self._parent_resource.model_object_key)
+
+        return parent_ids
+
+    def get_parent_object(self, obj):
+        """Returns the parent of an object.
+
+        By default, this uses ``model_parent_key`` to figure out the parent,
+        but it can be overridden for more complex behavior.
+        """
+        parent_obj = getattr(obj, self.model_parent_key)
+
+        if isinstance(parent_obj, (models.Manager, models.ForeignKey)):
+            parent_obj = parent_obj.get()
+
+        return parent_obj
+
+    def _build_named_url(self, name):
+        """Builds a Django URL name from the provided name."""
+        return '%s-resource' % name.replace('_', '-')
+
+
+class RootResource(WebAPIResource):
+    """The root of a resource tree.
+
+    This is meant to be instantiated with a list of immediate child
+    resources. The result of ``get_url_patterns`` should be included in
+    a project's ``urls.py``.
+    """
+    name = 'root'
+    name_plural = 'root'
+
+    def __init__(self, child_resources=[], include_uri_templates=True):
+        super(RootResource, self).__init__()
+        self.list_child_resources = child_resources
+        self._uri_templates = {}
+        self._include_uri_templates = include_uri_templates
+
+    def get_list(self, request, *args, **kwargs):
+        data = {
+            'links': self.get_links(self.list_child_resources,
+                                    request=request, *args, **kwargs),
+        }
+
+        if self._include_uri_templates:
+            data['uri_templates'] = self.get_uri_templates(request, *args,
+                                                           **kwargs)
+
+        return 200, data
+
+    def get_uri_templates(self, request, *args, **kwargs):
+        """Returns all URI templates in the resource tree.
+
+        REST APIs can be very chatty if a client wants to be well-behaved
+        and crawl the resource tree asking for the links, instead of
+        hard-coding the paths. The benefit is that they can keep from
+        breaking when paths change. The downside is that it can take many
+        HTTP requests to get the right resource.
+
+        This list of all URI templates allows clients who know the resource
+        name and the data they care about to simply plug them into the
+        URI template instead of trying to crawl over the whole tree. This
+        can make things far more efficient.
+        """
+        if not self._uri_templates:
+            self._uri_templates = {}
+            base_href = request.build_absolute_uri()
+
+            for name, href in self._walk_resources(self, base_href):
+                self._uri_templates[name] = href
+
+        return self._uri_templates
+
+    def _walk_resources(self, resource, list_href):
+        yield resource.name_plural, list_href
+
+        for child in resource.list_child_resources:
+            child_href = list_href + child.uri_name + '/'
+
+            for name, href in self._walk_resources(child, child_href):
+                yield name, href
+
+        if resource.uri_object_key:
+            object_href = '%s{%s}/' % (list_href, resource.uri_object_key)
+
+            yield resource.name, object_href
+
+            for child in resource.item_child_resources:
+                child_href = object_href + child.uri_name + '/'
+
+                for name, href in self._walk_resources(child, child_href):
+                    yield name, href
+
+
+class UserResource(WebAPIResource):
+    """A default resource for representing a Django User model."""
+    model = User
+    fields = (
+        'id', 'username', 'first_name', 'last_name', 'fullname',
+        'email', 'url'
+    )
+
+    uri_object_key = 'username'
+    uri_object_key_regex = '[A-Za-z0-9_-]+'
+    model_object_key = 'username'
+
+    allowed_methods = ('GET',)
+
+    def serialize_fullname_field(self, user):
+        return user.get_full_name()
+
+    def serialize_url_field(self, user):
+        return user.get_absolute_url()
+
+    def has_modify_permissions(self, request, user, *args, **kwargs):
+        """Returns whether or not the user can modify this object."""
+        return request.user.is_authenticated() and user.pk == request.user.pk
+
+
+class GroupResource(WebAPIResource):
+    """A default resource for representing a Django Group model."""
+    model = Group
+    fields = ('id', 'name')
+
+    uri_object_key = 'group_name'
+    uri_object_key_regex = '[A-Za-z0-9_-]+'
+    model_object_key = 'name'
+
+    allowed_methods = ('GET',)
+
+
+user_resource = UserResource()
+group_resource = GroupResource()
