O Node.js segue a CommonJS, uma especificação de ecossistemas para o JavaScript, e a função embutida require é a maneira mais fácil de incluir módulos existentes em arquivos separados. O funcionamento básico do require é que ele lê o arquivo JavaScript, interpreta o script e em seguida retorna o conteúdo do objeto exports. Segue um exemplo de módulo para melhor compreensão:

console.log("Avaliando o exemplo.js");

var invisivel = function () {
  console.log("invisivel");
}

exports.mensagem = "Oi";

exports.falar = function () {
  console.log(exports.mensagem);
}

Podemos testar nosso módulo rodando através do REPL do node. Se formos na pasta onde salvamos nosso projeto exemplo.js com o prompt e abrirmos o REPL do node, para isso basta digitar node sem passar nenhum arquivo como parâmetro e ele abrirá em modo REPL, em seguida basta digitar require("./exemplo.js") para importar nosso módulo. Teremos a seguinte saída impressa:

> require("./exemplo.js")
Avaliando o exemplo.js
{ mensagem: 'Oi', falar: [Function] }

Aqui é possível ver que o módulo foi importado com sucesso, e como foi dito a função require primeiro interpretou o script, imprimindo Avaliando o exemplo.js na primeira linha, e em seguida retornando o conteúdo do objeto exports, que contém apenas dois objetos { mensagem: 'Oi', falar: [Function] }. A função invisivel não foi importada pois não foi atribuída ao objeto exports.

Porém caso quisermos utilizar a função falar de nosso módulo veremos que ela não estará disponível no contexto global, e a mensagem de erro dirá que falar não está definida.

> falar()
ReferenceError: falar is not defined
...

Para que seja possível utilizar as funções importadas de um módulo temos que importá-lo e atribuir o retorno da função require à uma variável. Se rodarmos no REPL o código var exemplo = require('./exemplo.js') ocorrerá que agora nosso script exemplo.js será avaliado e o objeto exemplo receberá { mensagem: 'Oi', falar: [Function] }, que é o retorno da função, assim é possível facilmente acessar sua função falar. Acompanhe o exemplo:

> exemplo = require("./exemplo.js")
Avaliando o exemplo.js
{ mensagem: 'Oi', falar: [Function] }
> exemplo.falar()
Oi
undefined

Pode ser que em seu console não apareça a mensagem Avaliando o exemplo.js novamente, você seberá o motivo mais adiante.

Se você quiser atribuir uma função ou um novo objeto para exports, então você terá que usar o objeto module.exports. Veja o exemplo do código exemplo2.js para compreender melhor.

module.exports = function () {
  console.log("Ola mundo")
}

require('./exemplo2.js')()

Neste exemplo ao importar o módulo com require, primeiramente ele interpreta (avalia, executa) o código o que faz o módulo importar e chamar a sí próprio e ao final require retorna a função atribuida ao exports. Ao importar o módulo temos a saída esperada:

> require('./exemplo2.js')
Ola mundo
[Function]

A última linha [Function] confirma que o valor de retorno da chamada require('./exemplo2.js') é uma função e a linha Ola mundo confirma que o código foi interpretado antes de ser retornada esta função.

É importante dizer, também, que o código é interpretado apenas na primeira vez que ele é importado, se você importá-lo mais de uma vez a função require irá reutilziar o objeto exports que já está em salvo cache. Para ilustrar este ponto acompanhe o exemplo abaixo:

> require("./exemplo.js")
Avaliando o exemplo.js
{ mensagem: 'Oi', falar: [Function] }
> require("./exemplo.js")
{ mensagem: 'Oi', falar: [Function] }
> require("./exemplo.js").mensagem = "Ola"
'Ola'
> require("./exemplo.js")
{ mensagem: 'Ola', falar: [Function] }
> require("./exemplo.js").falar()
Ola
undefined

Como pode ser visto no exemplo, exemplo.js é interpretado apenas na primeira vez e todas as chamadas posteriores à require() só invoca o módulo salvo em cache, em vez de ler o arquivo novamente. No exemplo é possível ver que isso pode causar efeitos colaterais caso utilizado indevidamente.

As regras que o require usa para buscar os arquivos pode ser um pouco complexa, mas uma regra de ouro é que se o arquivo não inicia com ./ ou /, então ele é considerado um módulo do core e o endereço local do Node é verificado, ou uma dependência na pasta local node_modules. Se o arquivo começa com ./ então ele é considerado um arquivo relativo com o arquivo que chamou o require. Se o arquivo começa com / então ele é considerado pertencente ao endereço absoluto.

Observação:
– Você pode omitir o .js, o require vai automáticamente adicioná-lo caso for necessário.
– Se o nome do arquivo passado para require é o nome de um diretório, a função vai primeiramente buscar por package.json no diretório e então carregar o arquivo referenciado na propriedade main. Caso contrário a função irá procurar por um arquivo chamado index.js dentro da pasta.

Para informações mais detalhadas veja o artigo detalhado sobre módulos em Node.js.