How should I implement hook_menu()?
This information is valid for Drupal 6 and 7. In Drupal 8, hook_menu() has been replaced by new routing system. Below we implement hook_menu() in three simple steps.

Step one

Create an empty module following the instructions in How to create an empty module. In the code shown here, it's assumed the module is named helloworld.

Step two

Add the following code to the module file.

/**
 * Implements hook_menu().
 */
function helloworld_menu() {
  $items['hello'] = array(
    'title' =&gt; 'Hello world!',
    'page callback' =&gt; 'helloworld_page',
    'access callback' =&gt; TRUE,
  );

  return $items;
}

/**
 * Page callback for /hello.
 */
function helloworld_page() {
  return 'Hello world!';
}


Step three

Enable the module, and visit http://example.com/hello. (Replace example.com with the domain name for your server.)
You should see the message "Hello world!". That's it! You have a fully working hook_menu() implementation. What follows are various more advanced topics regarding hook_menu(). In particular, you may want to read about permissions, as the page above will be viewable by anyone.

Arguments

If you want to pass more data to the page callback, you can use page arguments to achieve this. Page arguments should be an array of arguments to pass on to the page callback. If an integer is used as an argument it will represent a part of the URL, starting from 0, incremented once for each slash (/). In the following example, this means the 0 will be turned into 'hello'.

function helloworld_menu() {
  $items['hello'] = array(
    'page callback' =&gt; 'helloworld_page',
    'page arguments' =&gt; array(0),
  );

  return $items;
}

function helloworld_page($argument1) {
  return $argument1;
}


Strings will be sent on verbatim, so array(0, 'world') could be used to get hello world out again.

function helloworld_page($argument1, $argument2) {
  return $argument1 . ' ' . $argument2;
}


"Wildcards" can be used to accept arbitrary data from the URL.

function helloworld_menu() {
  $items['hello/%'] = array(
    'page callback' =&gt; 'helloworld_page',
    'page arguments' =&gt; array(1),
  );

  return $items;
}

function helloworld_page($argument1) {
  return $argument1;
}


Visiting hello/world, $argument1 will equal world.

Argument autoloading

Often a URL argument will be the number identifying, for example, an entity. To avoid duplicating code that converts this ID into its corresponding object, Drupal support autoloading for "named" wildcards. When a named wildcard is used, Drupal will check for a function with the same name as the wildcard, suffixed by _load. If such a function is found, it will be called with the value of the value in the URL, and whatever is returned by the loader function will be passed to the page callback in place of the original value. Since Drupal already has such a function for loading nodes, node_load(), we can get nodes autoloaded and passed to the page callback.

function helloworld_menu() {
  $items['hello/%node'] = array(
    'page callback' =&gt; 'helloworld_page',
    'page arguments' =&gt; array(1),
  );

  return $items;
}

function helloworld_page($node) {
  return t('Hello node (ID = !nid)', array('!nid' =&gt; $node-&gt;nid));
}


Advanced autoloading

Sometimes, it will be necessary to auto-load more based on more than one argument. Since only the named argument is passed to the loader by default, one needs to explicitly tell Drupal which extra load arguments should be passed on to the loader. For example, to load a specific revision of a node, it is necessary to pass to node_load() a node ID and a revision ID. That can be accomplished by the following code.

function helloworld_menu() {
  $items['hello/%node/revision/%'] = array(
    'page callback' =&gt; 'helloworld_page',
    'page arguments' =&gt; array(1),
    'load arguments' =&gt; array(3),
  );

  return $items;
}

function helloworld_page($node) {
  return t('Hello node (ID = !nid, revision ID = !rid)', array('!nid' =&gt; $node-&gt;nid, '!rid' =&gt; $node-&gt;vid));
}


Permissions

'access callback' =&gt; TRUE, is necessary to make the simple example above viewable at all, but it's hardly ideal, as it allows no control what so ever. Anyone that tries to visit /hello will be granted access. The easiest way to provide some measure of control, is to provide a access callback, much like the page callback from above. The following code still allows access to anyone, but shows how to move the logic to a function called at access time, thereby allowing more complex logic.

/**
 * Implements hook_menu().
 */
function helloworld_menu() {
  $items['hello'] = array(
    'page callback' =&gt; 'helloworld_page',
    'access callback' =&gt; 'helloworld_access',
  );

  return $items;
}

/**
 * Access callback for /hello.
 */
function helloworld_access() {
  return TRUE;
}


This isn't necessarily the best way, as using a custom function will often needlessly duplicate code. A better way will be, most of the time, to use user_access(). Together the access callback it is possible to set access arguments. It is possible to require the page to be viewable from the users with the access user profiles permission with the following code.

/**
 * Implements hook_menu().
 */
function helloworld_menu() {
  $items['hello'] = array(
    'page callback' =&gt; 'helloworld_page',
    'access callback' =&gt; 'user_access',
    'access arguments' =&gt; array('access user profiles'),
  );

  return $items;
}


Since the access callback by default is user_access, it can be left out, as in the above code.

More advanced topics

The official hook_menu() documentation provides a lot more information about the most complex use-cases for the hook.