diff --git node_example/node_example.info node_example/node_example.info
index 7e234e8..5fa094f 100755
--- node_example/node_example.info
+++ node_example/node_example.info
@@ -6,3 +6,4 @@ version = VERSION
 core = 7.x
 files[] = node_example.module
 files[] = node_example.install
+files[] = node_example.test
diff --git node_example/node_example.install node_example/node_example.install
index 0e1f5f6..965a8b8 100755
--- node_example/node_example.install
+++ node_example/node_example.install
@@ -1,33 +1,200 @@
 <?php
 // $Id$
+/**
+ * @file
+ * install file for Node Example module.
+ *
+ * Most of the definition of the node type is in this module.
+ */
 
 /**
- * Implementation of hook_install().
+ * Implements hook_install().
+ *
+ * @see node_type_set_defaults()
+ * @see field_info_instance()
+ * @see field_update_instance()
+ * @see field_create_field()
+ * @see field_create_instance()
  */
 function node_example_install() {
-  drupal_install_schema('node_example');
+  // get the name of our localization function for translation during install
+  // http://api.drupal.org/api/function/get_t/7
+  $t = get_t();
+
+  // Define the node type.  This is similar to hook_node_info() in Drupal 6
+  $node_example = array(
+    'type' => 'node_example',
+    'name' => $t('Example Node'),
+    'base' => 'node_content',
+    'description' => $t('This is an example node type with a few fields.'),
+    'body_label' => $t('Example Description')
+  );
+
+  // Complete the node type definition by setting any defaults not explicitly
+  // declared above.
+  // http://api.drupal.org/api/function/node_type_set_defaults/7
+  $content_type = node_type_set_defaults($node_example);
+
+  // Save the content type
+  node_type_save($content_type);
+
+  // Load the instance definition for our content types body
+  // http://api.drupal.org/api/function/field_info_instance/7
+  $body_instance = field_info_instance('node', 'body', 'node_example');
+
+  // Add our example_node_list view mode to the body instance display by
+  // instructing the body to display as a summary
+  $body_instance['display']['example_node_list'] = array(
+    'label' => 'hidden',
+    'type' => 'text_summary_or_trimmed',
+  );
+
+  // Save our changes to the body field instance.
+  // http://api.drupal.org/api/function/field_update_instance/7
+  field_update_instance($body_instance);
+
+  // Create all the fields we are adding to our content type.
+  // http://api.drupal.org/api/function/field_create_field/7
+  foreach (node_example_installed_fields() as $field) {
+    field_create_field($field);
+  }
+
+  // Create all the instances for our fields.
+  // http://api.drupal.org/api/function/field_create_instance/7
+  foreach (node_example_installed_instances() as $instance) {
+    $instance['object_type'] = 'node';
+    $instance['bundle'] = $node_example['type'];
+    field_create_instance($instance);
+  }
 }
 
 /**
- * Implementation of hook_uninstall().
+ * All fields created by this content type.  The field is the default definition
+ * that lets Drupal know that a field exists.
+ *
+ * This is inside its own function so that it can be used in both hook_install()
+ * and hook_uninstall().
  */
-function node_example_uninstall() {
-  drupal_uninstall_schema('node_example');
+function node_example_installed_fields() {
+  $t = get_t();
+  return array(
+    'color' => array(
+      'field_name' => 'color',
+      'label'       => $t('The colors available for this object.'),
+      'cardinality' => 3,
+      'type'        => 'text',
+      'settings'    => array(
+        'max_length' => 60,
+      ),
+    ),
+    'quantity' => array(
+      'field_name'  => 'quantity',
+      'type'        => 'text',
+    ),
+    'image' => array(
+      'field_name' => 'image',
+      'type'       => 'image',
+      'cardinality' => 1,
+    ),
+  );
 }
 
 /**
- * Implementation of hook_schema().
+ * All instances created by this content type.  The instance lets Drupal know
+ * which widget to use to allow the user to enter data and how to react in
+ * different view modes.  We are going to display a page that uses a custom
+ * "node_example_list" view mode.  We will set a cardinality of three allowing
+ * our content type to give the user three color fields to enter data into.
+ *
+ * This is inside its own function so that it can be used in both hook_install()
+ * and hook_uninstall().
  */
-function node_example_schema() {
-  $schema['node_example'] = array(
-    'fields' => array(
-      'vid'      => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0),
-      'nid'      => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0),
-      'color'    => array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => ''),
-      'quantity' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0),
+function node_example_installed_instances() {
+  $t = get_t();
+  return array(
+    'color' => array(
+      'field_name' => 'color',
+      'label'       => $t('The colors available for this object.'),
+      'cardinality' => 3,
+      'widget'      => array(
+        'type'    => 'text_textfield',
+      ),
+      'display' => array(
+        'example_node_list' => array(
+          'label' => 'hidden',
+          'type' => 'node_example_colors',
+        ),
+      ),
+    ),
+    'quantity' => array(
+      'field_name'  => 'quantity',
+      'type'        => 'text',
+      'widget'      => array(
+        'type'    => 'text_textfield',
+      ),
+      'display' => array(
+        'example_node_list' => array(
+          'label' => 'hidden',
+          'type' => 'hidden',
+        ),
+      ),
+    ),
+    'image' => array(
+      'field_name'  => 'image',
+      'label'       => $t('Upload an image:'),
+      'required'    => FALSE,
+      'widget' => array(
+        'type'    => 'image_image',
+        'weight'  => 2.10,
+      ),
+      'display' => array(
+        'example_node_list' => array(
+          'label' => 'hidden',
+          'type' => 'image_link_content__thumbnail',
+        ),
+      ),
     ),
-    'primary key' => array('vid', 'nid'),
   );
+}
+
+
+/**
+ * Implementation of hook_uninstall().
+ *
+ */
+function node_example_uninstall() {
+  // Gather all the example content that might have been created while this
+  // module was enabled.  Simple selects still use db_query().
+  // http://api.drupal.org/api/function/db_query/7
+  $sql = 'SELECT nid FROM {node} n WHERE n.type = :type';
+  $result = db_query($sql, array(':type' => 'node_example'));
+  $nids = array();
+  foreach ($result as $row) {
+    $nids[] = $row->nid;
+  }
+
+  // Delete all the nodes at once
+  // http://api.drupal.org/api/function/node_delete_multiple/7
+  node_delete_multiple($nids);
 
-  return $schema;
-}
\ No newline at end of file
+  // Loop over each of our fields and delete them individually.
+  // http://api.drupal.org/api/function/field_delete_field/7
+  foreach (array_keys(node_example_installed_fields()) as $field) {
+    field_delete_field($field);
+  }
+
+  // Loop over each of our fields instances and delete them individually.
+  // http://api.drupal.org/api/function/field_delete_field/7
+  $instances = field_info_instances('node', 'node_example');
+  foreach ($instances as $instance_name => $instance) {
+    field_delete_instance($instance);
+  }
+
+  // Delete our content type
+  // http://api.drupal.org/api/function/node_type_delete/7
+  node_type_delete('node_example');
+
+  // Purge all field infromation
+  // http://api.drupal.org/api/function/field_purge_batch/7
+  field_purge_batch(1000);
+}
diff --git node_example/node_example.module node_example/node_example.module
index 60b2086..c9d8998 100755
--- node_example/node_example.module
+++ node_example/node_example.module
@@ -4,278 +4,152 @@
 /**
  * @file
  * This is an example outlining how a module can be used to define a new
- * node type.
+ * node type.  In Drupal 7 we move most of what was once needed in this file
+ * to the node_example.install file so that it can be managed efficiently.
  *
- * Our example node type will allow users to specify a "color" and a "quantity"
- * for their nodes; some kind of rudimentary inventory-tracking system, perhaps?
- * To store this extra information, we need an auxiliary database table.
+ * Our example node type will allow users to specify multiple "colors",
+ * a "quantity" and an "image" for their nodes; some kind of rudimentary
+ * inventory-tracking system, perhaps?
  *
- * Database definition:
- * @code
- *   CREATE TABLE node_example (
- *     vid int(10) unsigned NOT NULL default '0',
- *     nid int(10) unsigned NOT NULL default '0',
- *     color varchar(255) NOT NULL default '',
- *     quantity int(10) unsigned NOT NULL default '0',
- *     PRIMARY KEY (vid, nid),
- *     KEY `node_example_nid` (nid)
- *   )
- * @endcode
- */
-
-/**
- * Implementation of hook_node_info(). This function replaces hook_node_name()
- * and hook_node_types() from 4.6. Drupal 5 expands this hook significantly.
- *
- * This is a required node hook. This function describes the nodes provided by
- * this module.
+ * In previous versions of Drupal, "teaser" and "page" were node view modes.  In
+ * Drupal 7 we can define custom view modes to let the node know how it should
+ * return it's data.  This module declares a custom view mode called
+ * "example_node_list".
  *
- * The required attributes are:
- * - "name" provides a human readable name for the node,
- * - "module" tells Drupal how the module's functions map to hooks (i.e. if the
- *   module is node_example_foo then node_example_foo_insert will be called
- *   when inserting the node).
- * - "description" provides a brief description of the node type, which is
- *   show up when a user accesses the "Create content" page for that node type.
+ * We no longer need an external database table to store this content types
+ * information.
  *
- * The other optional, attributes:
- * - "has_title" boolean that indicates whether or not this node type has a
- *   title field.
- * - "title_label": the label for the title field of this content type.
- * - "has_body": boolean that indicates whether or not this node type has a
- *   body field.
- * - "body_label": the label for the body field of this content type.
- * - "min_word_count": the minimum number of words for the body field to be
- *   considered valid for this content type.
+ * Remember that most node types do not require any custom code, as one
+ * simply creates them using fields.
  */
-function node_example_node_info() {
-  return array(
-    'node_example' => array(
-      'name' => t('Example node'),
-      'module' => 'node_example',
-      'description' => t("This is an example node type with a few fields."),
-      'has_title' => TRUE,
-      'title_label' => t('Example Title'),
-      'has_body' => TRUE,
-      'body_label' => t('Example Body'),
-    )
-  );
-}
-
-/**
- * Implementation of hook_access().
- *
- * Node modules may implement node_access() to determine the operations
- * users may perform on nodes. This example uses a very common access pattern.
- */
-function node_example_access($op, $node, $account) {
-  if ($op == 'create') {
-    return user_access('create example content', $account);
-  }
-
-  if ($op == 'update') {
-    if (user_access('edit any example content', $account) || (user_access('edit own example content', $account) && ($account->uid == $node->uid))) {
-      return TRUE;
-    }
-  }
-
-  if ($op == 'delete') {
-    if (user_access('delete any example content', $account) || (user_access('delete own example content', $account) && ($account->uid == $node->uid))) {
-      return TRUE;
-    }
-  }
-}
 
 /**
- * Implementation of hook_perm().
+ * Implements hook_menu().
  *
- * Since we are limiting the ability to create new nodes to certain users,
- * we need to define what those permissions are here. We also define a permission
- * to allow users to edit the nodes they created.
+ * We are providing a default page to illustrate the use of our custom node view
+ * mode that will live at http://example.com/?q=examples/node_example
  */
-function node_example_perm() {
-  return array(
-    'create example content',
-    'delete own example content',
-    'delete any example content',
-    'edit own example content',
-    'edit any example content',
+function node_example_menu() {
+  $items['examples/node_example'] = array(
+    'page callback' => 'node_example_page',
+    'access arguments' => array('access content'),
+    'title' => 'Node Example',
   );
+  return $items;
 }
 
 /**
- * Implementation of hook_form().
+ * Custom callback that builds our content and returns it to the browser.
+ *
+ * @return
+ *   a build array
  *
- * Now it's time to describe the form for collecting the information
- * specific to this node type. This hook requires us to return an array with
- * a sub array containing information for each element in the form.
  */
-function node_example_form(&$node) {
-  // The site admin can decide if this node type has a title and body, and how
-  // the fields should be labeled. We need to load these settings so we can
-  // build the node form correctly.
-  $type = node_get_types('type', $node);
-
-  if ($type->has_title) {
-    $form['title'] = array(
-      '#type' => 'textfield',
-      '#title' => check_plain($type->title_label),
-      '#required' => TRUE,
-      '#default_value' => $node->title,
-      '#weight' => -5
-    );
-  }
+function node_example_page() {
+  $build = array();
+  $sql = 'SELECT nid FROM {node} n WHERE n.type = :type AND n.status = :status';
+  $result = db_query($sql,
+    array(
+      ':type' => 'node_example',
+      ':status' => 1,
+    )
+  );
 
-  if ($type->has_body) {
-    // In Drupal 6, we can use node_body_field() to get the body and filter
-    // elements. This replaces the old textarea + filter_form() method of
-    // setting this up. It will also ensure the teaser splitter gets set up
-    // properly.
-    $form['body_field'] = node_body_field($node, $type->body_label, $type->min_word_count);
+  // Loop through each of our node_example nodes and instruct node_view
+  // to use our custom "example_node_list" view.
+  // http://api.drupal.org/api/function/node_load/7
+  // http://api.drupal.org/api/function/node_view/7
+  foreach ($result as $row) {
+    $node = node_load($row->nid);
+    $build['node_list'][]= node_view($node, 'example_node_list');
   }
 
-  // Now we define the form elements specific to our node type.
-  $form['color'] = array(
-    '#type' => 'textfield',
-    '#title' => t('Color'),
-    '#default_value' => isset($node->color) ? $node->color : '',
-  );
-  $form['quantity'] = array(
-    '#type' => 'textfield',
-    '#title' => t('Quantity'),
-    '#default_value' => isset($node->quantity) ? $node->quantity : 0,
-    '#size' => 10,
-    '#maxlength' => 10
-  );
-
-  return $form;
+  return $build;
 }
 
 /**
- * Implementation of hook_validate().
+ * Implements hook_entity_info_alter().
  *
- * Our "quantity" field requires a number to be entered. This hook lets
- * us ensure that the user entered an appropriate value before we try
- * inserting anything into the database.
+ * We need to modify the default node entity info by adding a new view mode to
+ * be used in functions like node_view() or node_build_content().
  *
- * Errors should be signaled with form_set_error().
  */
-function node_example_validate(&$node) {
-  if ($node->quantity) {
-    if (!is_numeric($node->quantity)) {
-      form_set_error('quantity', t('The quantity must be a number.'));
-    }
-  }
-  else {
-    // Let an empty field mean "zero."
-    $node->quantity = 0;
-  }
+function node_example_entity_info_alter(&$entity_info) {
+  $entity_info['node']['view modes']['example_node_list'] = array(
+    'label' => t('Example Node List'),
+  );
 }
 
-/**
- * Implementation of hook_insert().
- *
- * As a new node is being inserted into the database, we need to do our own
- * database inserts.
- */
-function node_example_insert($node) {
-  db_query("INSERT INTO {node_example} (vid, nid, color, quantity) VALUES (%d, %d, '%s', %d)", $node->vid, $node->nid, $node->color, $node->quantity);
-}
 
 /**
- * Implementation of hook_update().
- *
- * As an existing node is being updated in the database, we need to do our own
- * database updates.
+ * Implements hook_field_formatter_info().
  */
-function node_example_update($node) {
-  // if this is a new node or we're adding a new revision,
-  if ($node->revision) {
-    node_example_insert($node);
-  }
-  else {
-    db_query("UPDATE {node_example} SET color = '%s', quantity = %d WHERE vid = %d", $node->color, $node->quantity, $node->vid);
-  }
+function node_example_field_formatter_info() {
+  return array(
+    'node_example_colors' => array(
+      'label' => t('Node Example Color Handle'),
+      'field types' => array('text'),
+    ),
+  );
 }
 
 /**
- * Implementation of hook_nodeapi().
+ * Implements hook_field_formatter_view().
  *
- * When a node revision is deleted, we need to remove the corresponding record
- * from our table. The only way to handle revision deletion is by implementing
- * hook_nodeapi().
+ * @todo: We need to provide a formatter for the colors that a user is allowed to enter
+ * during node creation.
  */
-function node_example_nodeapi(&$node, $op, $teaser, $page) {
-  switch ($op) {
-    case 'delete_revision':
-      // Notice that we're matching a single revision based on the node's vid.
-      db_query('DELETE FROM {node_example} WHERE vid = %d', $node->vid);
+function node_example_field_formatter_view($object_type, $object, $field, $instance, $langcode, $items, $display) {
+  $element = array();
+  switch ($display['type']) {
+    case 'node_example_colors':
+      foreach ($items as $delta => $item) {
+        $element[$delta]['#type'] = 'markup';
+        $color = $item['safe_value'];
+        $element[$delta]['#markup'] = theme('example_node_color', array('color' => $color));
+      }
       break;
   }
-}
 
-/**
- * Implementation of hook_delete().
- *
- * When a node is deleted, we need to remove all related records from our table.
- */
-function node_example_delete($node) {
-  // Notice that we're matching all revision, by using the node's nid.
-  db_query('DELETE FROM {node_example} WHERE nid = %d', $node->nid);
+  return $element;
 }
 
 /**
- * Implementation of hook_load().
+ * Implements hook_theme().
  *
- * Now that we've defined how to manage the node data in the database, we
- * need to tell Drupal how to get the node back out. This hook is called
- * every time a node is loaded, and allows us to do some loading of our own.
- */
-function node_example_load($node) {
-  $additions = db_fetch_object(db_query('SELECT color, quantity FROM {node_example} WHERE vid = %d', $node->vid));
-  return $additions;
-}
-
-/**
- * Implementation of hook_view().
- *
- * This is a typical implementation that simply runs the node text through
- * the output filters.
+ * This lets us tell Drupal about our theme functions and their arguments.
  */
-function node_example_view($node, $teaser = FALSE, $page = FALSE) {
-  $node = node_prepare($node, $teaser);
-  $node->content['myfield'] = array(
-    '#value' => theme('node_example_order_info', $node),
-    '#weight' => 1,
+function node_example_theme($existing, $type, $theme, $path) {
+  return array(
+    'example_node_color' => array(
+      'variables' => array('color' => NULL),
+    ),
   );
-
-  return $node;
 }
 
 /**
- * Implementation of hook_theme().
- *
- * This lets us tell Drupal about our theme functions and their arguments.
+ * Implements hook_help().
  */
-function node_example_theme() {
-  return array(
-    'node_example_order_info' => array(
-      'arguments' => array('node'),
-    ),
-  );
+function node_example_help($path, $arg) {
+  switch ($path) {
+    case 'examples/node_example':
+      return "<p>" . t(
+        "The Node Example module provides a custom node type.
+        You can create new nodes using the <a href='!nodeadd'>node add form</a>.
+        Nodes that you create will be displayed here.",
+        array('!nodeadd' => url('node/add/node-example'))
+      ) . "</p>";
+  }
 }
 
 /**
  * A custom theme function.
  *
  * By using this function to format our node-specific information, themes
- * can override this presentation if they wish. We also wrap the default
- * presentation in a CSS class that is prefixed by the module name. This
- * way, style sheets can modify the output without requiring theme code.
+ * can override this presentation if they wish.  This is a simplifed theme
+ * function purely for illustrative purposes.
  */
-function theme_node_example_order_info($node) {
-  $output = '<div class="node_example_order_info">';
-  $output .= t('The order is for %quantity %color items.', array('%quantity' => check_plain($node->quantity), '%color' => check_plain($node->color)));
-  $output .= '</div>';
+function theme_example_node_color($variables) {
+  $output = '<span style="background-color: #ccc; padding: 1em; margin-bottom: 1em; float: left; color: ' . $variables['color'] . '">' . $variables['color'] . '</span>';
   return $output;
 }
diff --git node_example/node_example.test node_example/node_example.test
new file mode 100644
index 0000000..b283d5e
--- /dev/null
+++ node_example/node_example.test
@@ -0,0 +1,56 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Simpletest case for node_example module.
+ *
+ * Verify example module functionality.
+ */
+
+/**
+ * Functionality tests for node example module.
+ */
+class NodeExampleTestCase extends DrupalWebTestCase {
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Node example',
+      'description' => 'Verify the custom node type creation.',
+      'group' => 'Examples',
+    );
+  }
+
+  function setUp() {
+    // Enable the module.
+    parent::setUp('node_example');
+  }
+
+  /**
+   * Verify the functionality of the example module.
+   */
+  function testNodeCreation() {
+    // Create and login user.
+    $account = $this->drupalCreateUser(array('access content', 'create node_example content'));
+    $this->drupalLogin($account);
+
+    // Create a new node. The image makes it more complicated, so skip it.
+    $edit = array(
+      'title' => $this->randomName(),
+      'color[und][0][value]' => 'red',
+      'color[und][1][value]' => 'green',
+      'color[und][2][value]' => 'blue',
+      'quantity[und][0][value]' => 100,
+    );
+    $this->drupalPost('node/add/node-example', $edit, t('Save'));
+    $this->assertText("Example Node " . $edit['title'] . " has been created", "Found node creation message");
+    $this->assertPattern("/The colors available.*red.*green.*blue/", "Correct 'colors available' on node page");
+
+    // Look on the examples page to make sure it shows up there also.
+    $this->drupalGet('examples/node_example');
+    $this->assertText($edit['title'], "Found random title string");
+    $this->assertPattern("/red.*green.*blue/", "Correct 'colors available' on node example page");
+
+  }
+}
+
