diff --git a/src/model.ts b/src/model.ts
index 9830d0a7286a86108aa619ba4e6912c359404dba..6a4c7299d7c89ab8944d3b9c48e3dcf299930be1 100644
--- a/src/model.ts
+++ b/src/model.ts
@@ -9,6 +9,15 @@ import * as Backbone from 'backbone';
 import { spinaBaseClassExtends } from './objects';
 
 
+/**
+ * A base type for attributes or defaults for a model.
+ *
+ * Version Added:
+ *     2.0
+ */
+export type ModelAttributes = Backbone.ObjectHash;
+
+
 /**
  * Base class for models.
  *
@@ -20,9 +29,11 @@ import { spinaBaseClassExtends } from './objects';
  * default (``@types/backbone`` has a default of ``any``, which removes all
  * type safety for options).
  */
-export abstract class BaseModel<T extends Backbone.ObjectHash = any,
-                                E = unknown,
-                                S = Backbone.ModelSetOptions>
+export abstract class BaseModel<
+    TDefaults extends ModelAttributes = ModelAttributes,
+    TExtraModelOptions = unknown,
+    TModelOptions = Backbone.ModelSetOptions
+>
 extends spinaBaseClassExtends(
     Backbone.Model,
     {
@@ -32,24 +43,27 @@ extends spinaBaseClassExtends(
         prototypeAttrs: [
             'defaults',
             'idAttribute',
+            'urlRoot',
         ],
     }
-)<T, S, E> {
+)<TDefaults, TModelOptions, TExtraModelOptions> {
     /**
      * The defined default attributes for the model.
      *
-     * If provided, this must be defined as static.
+     * If provided, this must be static. It can be a hash of attribute names
+     * to values, or a function that returns a hash. The function can access
+     * ``this``.
      *
      * Version Added:
      *     2.0:
      *     Starting in Spina 2, this must be defined as static.
      */
-    static defaults: Backbone.ObjectHash = {};
+    static defaults: Backbone._Result<Partial<ModelAttributes>> = {};
 
     /**
      * The name of the ID attribute to set.
      *
-     * If provided, this must be defined as static.
+     * If provided, this must be static.
      *
      * Version Added:
      *     2.0:
@@ -57,15 +71,15 @@ extends spinaBaseClassExtends(
      */
     static idAttribute: string = 'id';
 
-
-    /**********************
-     * Instance variables *
-     **********************/
-
-    /*
-     * These variables above are copied to the prototype and made available
-     * to the instance. Declare them to help with type checking.
+    /**
+     * The root for any relative URLs.
+     *
+     * If provided, this must be static. It can be a string with the URL root,
+     * or a function that returns a string. The function can access ``this``.
+     *
+     * Version Added:
+     *     2.0:
+     *     Starting in Spina 2, this must be defined as static.
      */
-    declare defaults: Backbone._Result<Partial<T>>;
-    declare idAttribute: string;
+    static urlRoot: Backbone._Result<string>;
 }
diff --git a/src/tests/baseModelTests.ts b/src/tests/baseModelTests.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7b604c251c9bc89c6bb04b2e323efd0d0b9b5e55
--- /dev/null
+++ b/src/tests/baseModelTests.ts
@@ -0,0 +1,200 @@
+import 'jasmine';
+import _ from 'underscore';
+
+import {
+    BaseModel,
+    ModelAttributes,
+    spina,
+} from '../index';
+
+
+interface MyModel3Options {
+    myExtraDefaults: ModelAttributes;
+    myURL: string;
+}
+
+
+/* The first test model contains basic static attributes. */
+@spina
+class MyModel extends BaseModel {
+    static defaults: ModelAttributes = {
+        'attr1': 123,
+        'attr2': 'abc',
+    };
+
+    static idAttribute = 'myID';
+
+    static url = '/api/collection/';
+}
+
+
+/* The second test model extends the first with merged attributes. */
+@spina
+class MyModel2 extends MyModel {
+    static defaults: ModelAttributes = {
+        'attr3': true,
+        'attr4': null,
+    };
+}
+
+
+/* The third test model uses static methods utilizing instance data. */
+@spina
+class MyModel3 extends BaseModel<ModelAttributes, MyModel3Options> {
+    static defaults(
+        this: MyModel3,
+    ): ModelAttributes {
+        return Object.assign({
+            'dictKey': {
+                'a': 1,
+            },
+            'listKey': [1],
+        }, this.options.myExtraDefaults);
+    }
+
+    static url(
+        this: MyModel3,
+    ): string {
+        return this.options.myURL;
+    }
+
+    options: MyModel3Options;
+
+    preinitialize(
+        attrs: ModelAttributes,
+        options: MyModel3Options,
+    ) {
+        this.options = options;
+    }
+}
+
+
+describe('BaseModel', () => {
+    describe('Static-defined attributes', () => {
+        describe('Access', () => {
+            it('On prototype', () => {
+                const proto = MyModel.prototype;
+
+                expect(proto.defaults).toEqual({
+                    'attr1': 123,
+                    'attr2': 'abc',
+                });
+                expect(proto.idAttribute).toBe('myID');
+            });
+
+            it('On instance', () => {
+                const model = new MyModel();
+
+                expect(model.defaults).toEqual({
+                    'attr1': 123,
+                    'attr2': 'abc',
+                });
+                expect(model.idAttribute).toBe('myID');
+            });
+        });
+
+        describe('Merging', () => {
+            it('On class', () => {
+                expect(MyModel2.defaults).toEqual({
+                    'attr1': 123,
+                    'attr2': 'abc',
+                    'attr3': true,
+                    'attr4': null,
+                });
+            });
+
+            it('On prototype', () => {
+                const proto = MyModel2.prototype;
+
+                expect(proto.defaults).toEqual({
+                    'attr1': 123,
+                    'attr2': 'abc',
+                    'attr3': true,
+                    'attr4': null,
+                });
+            });
+
+            it('On instance', () => {
+                const model = new MyModel2();
+
+                expect(model.defaults).toEqual({
+                    'attr1': 123,
+                    'attr2': 'abc',
+                    'attr3': true,
+                    'attr4': null,
+                });
+            });
+        });
+    });
+
+    describe('Static-defined attribute functions', () => {
+        describe('Access', () => {
+            it('On prototype', () => {
+                const proto = MyModel3.prototype;
+
+                expect(proto.defaults).toBeInstanceOf(Function);
+                expect(proto.url).toBeInstanceOf(Function);
+            });
+
+            it('On instance', () => {
+                const model1 = new MyModel3({}, {
+                    myExtraDefaults: {
+                        'myExtra': [1, 2, 3],
+                    },
+                    myURL: '/api/v2/collection/',
+                });
+
+                expect(model1.defaults).toBeInstanceOf(Function);
+                expect(_.result(model1, 'defaults')).toEqual({
+                    'dictKey': {
+                        'a': 1,
+                    },
+                    'listKey': [1],
+                    'myExtra': [1, 2, 3],
+                });
+                expect(model1.attributes).toEqual({
+                    'dictKey': {
+                        'a': 1,
+                    },
+                    'listKey': [1],
+                    'myExtra': [1, 2, 3],
+                });
+
+                /*
+                 * Let's modify those to make sure they don't carry over to
+                 * other instances.
+                 */
+                model1.attributes['listKey'].push(2);
+                model1.attributes['dictKey']['b'] = 'c';
+                model1.attributes['myExtra'].push('a');
+
+                /* Now create a new instance. */
+                const model2 = new MyModel3({}, {
+                    myExtraDefaults: {
+                        'myExtra': [1, 2, 3],
+                        'myExtra2': true,
+                    },
+                    myURL: '/api/v3/collection/',
+                });
+
+                expect(model2.defaults).toBeInstanceOf(Function);
+                expect(_.result(model2, 'defaults')).toEqual({
+                    'dictKey': {
+                        'a': 1,
+                    },
+                    'listKey': [1],
+                    'myExtra': [1, 2, 3],
+                    'myExtra2': true,
+                });
+                expect(model2.attributes).toEqual({
+                    'dictKey': {
+                        'a': 1,
+                    },
+                    'listKey': [1],
+                    'myExtra': [1, 2, 3],
+                    'myExtra2': true,
+                });
+            });
+        });
+    });
+});
