José Navarro é um desenvolvedor full stack na FAMOCO em Bruxelas, Bélgica. Ele tem trabalhado nos últimos 3 anos como desenvolvedor web desenvolvedor com Node.js, Java, AngularJS e ReactJS, e tem grande interesse em desenvolvimento web e tecnologias móveis.
Introdução
Vamos desenvolver uma API REST usando o Node.js e o Couchbase ODM Ottoman. Existem algumas estruturas para fazer isso no Node.js, portanto, usaremos o hapi.js, que facilita o início e o desenvolvimento de uma API, e seu código é limpo e fácil de entender. Ele também fornece um validador na solicitação para que possamos nos integrar bem ao modelo Ottoman, que usaremos para abstrair nosso código e trabalhar com objetos.
Requisitos
Para criar o projeto, você precisa ter os seguintes itens instalados em seu computador:
-
Node.js e NPM
-
Servidor Couchbase
Servidor Hapi
Primeiro, criamos o diretório principal do nosso projeto e, em seguida, entramos nesse diretório e iniciamos o projeto npm, no qual serão solicitados alguns parâmetros para o nosso projeto.
Podemos fazer isso com os seguintes comandos:
mkdir node–hapi–couchbase–API
npm init
A próxima etapa é adicionar as dependências ao nosso projeto. Primeiro, adicionaremos a dependência hapi.js em seguida, adicionamos os pacotes relacionados ao Couchbase e, por fim, adicionamos nodemônio às nossas dependências de desenvolvimento para o recarregamento em tempo real do nosso servidor enquanto estamos programando.
npm install –S hapi joi
npm install –Otomana com base de sofá S
npm install –D nodemon
Quando tudo isso estiver pronto, começaremos a criar nosso projeto. Criamos uma pasta src onde teremos todo o nosso código. Dentro dele, criamos um index.js onde teremos nosso servidor hapi básico. Nesse arquivo, adicionamos o seguinte código:
const Hapi = exigir("hapi);
// Criar um servidor com um host e uma porta
const servidor = novo Hapi.Servidor();
servidor.conexão({
hospedeiro: 'localhost',
porto: 5000,
rotas: {
cors: verdadeiro,
}
});
// Iniciar o servidor
servidor.iniciar( erro => {
se( erro ) {
// Tratamento de erros sofisticado aqui
console.erro( erro );
lançar err;
}
console.registro( O servidor foi iniciado em ${ server.info.uri }
);
} );
módulo.exportações = servidor;
Acabamos de criar nosso servidor básico.
Agora, vamos definir uma rota de entrada para o nosso servidor. Primeiro, criamos uma pasta API onde definiremos nossas rotas. E criamos um arquivo index.jscom o código de nossa rota de entrada:
const rotas = [
{
método: 'GET',
caminho: ‘/’,
configuração: {
manipulador: (solicitação, resposta) => {
retorno resposta({
nome: 'node-hapi-couchbase-api',
versão: 1
});
}
}
}
];
módulo.exportações = rotas;
No principal index.js vamos importar as rotas. Para isso, adicionamos o seguinte código antes do código server.start que definimos anteriormente:
const rotas = exigir('./api');
// Adicionar as rotas
servidor.rota(rotas);
Agora em nosso package.json adicionaremos o arquivo script seção.
"scripts": {
"start": "nodemon ./src/index.js"
},
Se executarmos npm startIniciaremos nosso servidor. Podemos verificar isso acessando http://localhost:5000e devemos receber uma resposta.
{"name" (nome):"node-hapi-couchbase-api","versão":1}
Conector de banco de dados
Para configurar o conector de banco de dados, vamos criar uma pasta db onde armazenaremos as informações do banco de dados e a lógica do conector.
Vamos armazenar as informações no arquivo config.json com o seguinte código:
{
"couchbase": {
"endpoint": "localhost:8091",
"bucket" (balde): "api"
}
}
Para o conector, vamos criar um arquivo index.jsonde importaremos o arquivo de configuração e a biblioteca do Couchbase e inicializaremos a conexão com o banco de dados e o bucket.
deixe a configuração = exigir('./config');
deixe couchbase = exigir('couchbase');
deixe o endpoint = configuração.couchbase.ponto final;
deixe o balde = configuração.couchbase.balde;
let myCluster = novo couchbase.Aglomerado(ponto final, função(erro) {
se (erro) {
console.registro("Não é possível se conectar ao couchbase: %s", erro);
}
console.registro('conectado ao db %s', ponto final);
});
let myBucket = myCluster.openBucket(balde, função(erro) {
se (erro) {
console.registro("Não é possível se conectar ao bucket: %s", erro);
}
console.registro('conectado ao bucket %s', balde);
});
A próxima etapa é importar o Couchbase ODM Ottoman e configurá-lo com o bucket.
deixar otomano = exigir("otomano);
otomano.loja = novo otomano.CbStoreAdapter(myBucket, couchbase);
Por fim, vamos exportar o balde e a otomana para termos acesso a partir de outros arquivos.
módulo.exportações = {
balde: myBucket,
otomano: otomano
};
Modelos
Agora que temos nosso servidor básico em execução, vamos definir nossos modelos com o Ottoman. Vamos definir dois modelos: um para um Usuário e outro para um Postar. Para isso, criamos uma pasta chamada modelose, dentro dele, criamos dois arquivos js: usuário.js e post.js. Podemos adicionar as validações no modelo, mas o hapi.js oferece uma validação antes de manipular a rota, portanto, vamos usá-la para validar os dados que recebemos do usuário antes de passá-los para o nosso modelo.
Modelo de usuário
O usuário terá três campos: nome, e-mail, e senha. Criamos nosso modelo de usuário usando o pacote Ottoman. Nosso modelo de usuário contém o seguinte código:
deixar otomano = exigir('../db').Otomano;
deixar UserModel = otomano.modelo('Usuário', {
senha: 'string',
nome: 'string',
e-mail: 'string',
}, {
índice: {
findByEmail: {
por: 'email',
tipo: 'refdoc'
}
}
});
Primeiro, importamos a instância do Ottoman que iniciamos no conector db. Depois disso, começamos a definir nosso modelo. O primeiro parâmetro é o nome do nosso modelo, neste caso, "User" (Usuário). O segundo parâmetro é o objeto JSON que contém o nome do campo e o tipo; no nosso caso, todos os valores são string (verifique Ottoman para ver outros tipos). O próximo parâmetro é o objeto que contém o índice que queremos criar. Vamos criar um índice para o e-mail para que possamos usar esse índice para consultar o usuário usando nosso modelo; isso também criará uma restrição para evitar e-mails duplicados em nossos usuários.
Quando criamos um índice, precisamos chamar a função ensureIndices para criar os índices internamente.
otomano.ensureIndices(função(erro) {
se (erro) {
retorno console.erro('Error ensure indices USER', erro);
}
console.registro('Garantir índices USER');
});
A última etapa é exportar o modelo.
módulo.exportações = UserModel;
Modelo de postagem
A postagem conterá quatro campos: título e corpo, o carimbo de data/horae o usuário.
Primeiro, importamos a instância do Ottoman que inicializamos no conector db e também importamos o modelo User.
deixar otomano = exigir('../db').Otomano;
deixar Usuário = exigir('./usuário');
deixar PostModel = otomano.modelo('Postar', {
usuário: Usuário,
título: 'string',
corpo: 'string',
carimbo de data/hora: {
tipo: "Data",
padrão: Data.agora
}
});
O primeiro parâmetro é o nome do nosso modelo, "Post". O segundo é o objeto JSON com nosso campo. Nesse caso, definimos o usuário com o tipo Usuário que definimos em nosso modelo anterior; o título e o corpo do tipo stringe registro de data e hora do tipo Data. Vamos criar um valor padrão com o registro de data e hora atual quando o objeto for criado.
E, por fim, exportamos nosso modelo.
módulo.exportações = PostModel;
Rotas de API
Vamos definir nossas rotas para Usuários e Posts; o caminho básico que usaremos é /api/v1. Em nosso arquivo index.js dentro da API, importaremos as rotas de usuário e as rotas de postagem e as juntaremos em uma matriz.
const usuários = exigir('./usuários');
const postagens = exigir('./posts');
…
rotas = rotas.concatenar(usuários);
rotas = rotas.concatenar(postagens);
Nas rotas User e Post, definiremos os métodos para executar uma operação CRUD. Para cada rota, precisamos definir o método, o caminho e a configuração. Na seção de configuração, fornecemos o manipulador, que é a função a ser executada, e também podemos fornecer uma função de validação que será chamada antes de executarmos a função de manipulação. Para as validações, usaremos o pacote Joi, que pode ser usado para definir o esquema e as validações para o corpo da solicitação.
Rotas de usuário
Para os usuários, usaremos o caminho /api/v1/usuários. A primeira etapa do nosso arquivo de rotas é importar o modelo User e o pacote joi.
const Usuário = exigir('../models/user');
const Joi = exigir("joi);
Recuperar a lista de usuários GET /api/v1/users
Na função handle, usaremos a função find do modelo User, que nos permite consultar o banco de dados para coletar todos os documentos do tipo User.
{
método: 'GET',
caminho: '/api/v1/users',
configuração: {
manipulador: (solicitação, resposta) => {
Usuário.encontrar({}, (erro, usuários) => {
se (erro) {
retorno resposta({
status: 400,
mensagem: erro.mensagem
}).código(400);
}
retorno resposta({
dados: usuários,
contagem: usuários.comprimento
});
});
}
}
}
Vamos retornar um objeto com uma matriz de usuário e uma contagem com o número de objetos dentro da matriz.
Recuperar um usuário por seu ID GET /api/v1/users/{id}
Nesse caso, vamos consultar um usuário pelo ID do documento, portanto, usaremos a função integrada getById em nosso modelo para recuperar um documento do banco de dados.
Nesse caso, fornecemos um validar para validar que o valor do parâmetro id é uma string.
{
método: 'GET',
caminho: '/api/v1/users/{id}',
configuração: {
manipulador: (solicitação, resposta) => {
Usuário.getById(solicitação.parâmetros.id, (erro, usuário) => {
se (erro) {
retorno resposta({
status: 400,
mensagem: erro.mensagem
}).código(400);
}
retorno resposta(usuário);
});
},
validar: {
parâmetros: {
id: Joi.string(),
}
}
}
}
Vamos retornar o documento do usuário.
Criar um novo usuário POST /api/v1/users
Agora vamos criar um novo usuário. A primeira etapa é criar o usuário com o modelo User e o corpo da solicitação.
Fornecemos um objeto de validação para verificar esse payload (corpo da solicitação).
{
método: 'POST',
caminho: '/api/v1/users',
configuração: {
manipulador: (solicitação, resposta) => {
const usuário = novo Usuário(solicitação.carga útil);
usuário.salvar((erro) => {
se (erro) {
retorno resposta({
status: 400,
mensagem: erro.mensagem
}).código(400);
}
retorno resposta(usuário).código(201);
});
},
validar: {
carga útil: {
senha: Joi.string().alfanum().min(3).máximo(30).necessário(),
e-mail: Joi.string().e-mail().necessário(),
nome: Joi.string()
}
}
}
}
Retornaremos o objeto do novo usuário criado.
Atualizar um usuário PUT /api/v1/users/{id}
Agora vamos atualizar um usuário. Nesse caso, primeiro recuperaremos o documento do usuário no banco de dados, depois atualizaremos os campos e, por fim, salvaremos o documento atualizado no banco de dados.
Nesse caso, fornecemos um objeto validate em que validamos os parâmetros e a carga útil.
{
método: 'PUT',
caminho: '/api/v1/users/{id}',
configuração: {
manipulador: (solicitação, resposta) => {
Usuário.getById(solicitação.parâmetros.id, (erro, usuário) => {
se (erro) {
retorno resposta({
status: 400,
mensagem: erro.mensagem
}).código(400);
}
const carga útil = solicitação.carga útil;
se (carga útil.nome) {
usuário.nome = carga útil.nome;
}
se (carga útil.senha) {
usuário.senha = carga útil.senha;
}
usuário.salvar((erro) => {
se (erro) {
retorno resposta({
status: 400,
mensagem: erro.mensagem
}).código(400);
}
retorno resposta(usuário).código(200);
});
});
},
validar: {
parâmetros: {
id: Joi.string(),
},
carga útil: {
nome: Joi.string(),
senha: Joi.string().alfanum().min(3).máximo(30),
}
}
}
}
Devolveremos o documento atualizado.
Excluir um usuário DELETE /api/v1/users/{id}
Neste caso, vamos excluir um usuário. Primeiro, recuperamos o documento do banco de dados e, em seguida, o removemos.
{
método: 'DELETE',
caminho: '/api/v1/users/{id}',
configuração: {
manipulador: (solicitação, resposta) => {
Usuário.getById(solicitação.parâmetros.id, (erro, usuário) => {
se (erro) {
retorno resposta({
status: 400,
mensagem: erro.mensagem
}).código(400);
}
usuário.remover((erro) => {
se (erro) {
retorno resposta({
status: 400,
mensagem: erro.mensagem
}).código(400);
}
retorno resposta(usuário);
});
});
},
validar: {
parâmetros: {
id: Joi.string(),
}
}
}
}
Devolveremos o documento excluído.
Por fim, precisamos exportar as rotas.
módulo.exportações = rotas;
Rotas de postagem
Para as rotas de postagem, usaremos o caminho /api/v1/users/{userId}/posts, de modo que só realizaremos operações de postagem relacionadas ao usuário. Definiremos uma função de validação que verificará se o usuário existe no banco de dados e a retornará para que tenhamos acesso ao usuário na função que processa a solicitação.
A primeira seção do código são as importações e essa função.
const Usuário = exigir('../models/user');
const Postar = exigir('../models/post');
const Joi = exigir("joi);
const validateUser = (valor, opções, próxima) => {
const userId = opções.contexto.parâmetros.userId;
Usuário.getById(userId, (erro, usuário) => {
próxima(erro, Objeto.atribuir({}, valor, { usuário }))
})
};
Recuperar a lista de postagens do usuário GET /api/v1/users/{userId}/posts
Para recuperar todas as publicações do usuário, usaremos o modelo Post e a função find. Vamos executar com um objeto em que forneceremos o ID do usuário para recuperar todas as publicações do usuário.
{
método: 'GET',
caminho: '/api/v1/users/{userId}/posts',
configuração: {
manipulador: (solicitação, resposta) => {
const usuário = solicitação.consulta.usuário;
Postar.encontrar({ usuário: { _id: usuário._id } }, (erro, postagens) => {
se (erro) {
retorno resposta({
status: 400,
mensagem: erro.mensagem
}).código(400);
}
retorno resposta({
dados: postagens,
contagem: postagens.comprimento
});
})
},
validar: {
consulta: validateUser,
}
}
}
Vamos retornar um objeto com uma matriz de posts e a contagem de posts.
Recuperar uma postagem GET /api/v1/users/{userId}/posts/{postId}
Como fazemos na lista de publicações, para recuperar uma publicação, chamaremos a função find com o ID do usuário e também com o ID da publicação que queremos recuperar.
{
método: 'GET',
caminho: '/api/v1/users/{userId}/posts/{postId}',
configuração: {
manipulador: (solicitação, resposta) => {
const usuário = solicitação.consulta.usuário;
const postId = solicitação.parâmetros.postId;
Postar.encontrar({ usuário: { _id: usuário._id }, _id: postId }, (erro, postagens) => {
se (erro) {
retorno resposta({
status: 400,
mensagem: erro.mensagem
}).código(400);
}
se (postagens.comprimento === 0) {
retorno resposta({
status: 404,
mensagem: "Não encontrado
}).código(404);
} mais {
retorno resposta(postagens[0]);
}
})
},
validar: {
consulta: validateUser,
}
}
}
Vamos retornar a primeira postagem que recebermos. Só podemos receber uma postagem porque estamos consultando o banco de dados para encontrar uma postagem pelo seu ID, e é por isso que retornamos o primeiro item da matriz. Se não recebermos nenhuma postagem, isso significa que não há nenhuma postagem com esse ID relacionada a esse usuário, portanto, retornamos um erro de não encontrado.
Criar uma nova postagem POST /api/v1/users/{userId}/posts
Para criar uma postagem, faremos o mesmo processo que fizemos com o usuário. Fornecemos um objeto validate para a carga útil para que possamos validar o corpo que recebemos. Validamos apenas o título e o corpo da postagem porque o usuário do qual estamos obtendo - o caminho e o carimbo de data/hora - é gerado quando criamos a postagem.
Na função do manipulador, criamos uma nova postagem com a carga útil e definimos o usuário da postagem com o usuário.
{
método: 'POST',
caminho: '/api/v1/users/{userId}/posts',
configuração: {
manipulador: (solicitação, resposta) => {
const usuário = solicitação.consulta.usuário;
const postagem = novo Postar(solicitação.carga útil);
postagem.usuário = usuário;
postagem.salvar((erro) => {
se (erro) {
retorno resposta({
status: 400,
mensagem: erro.mensagem
}).código(400);
}
retorno resposta(postagem).código(201);
});
},
validar: {
consulta: validateUser,
carga útil: {
título: Joi.string().necessário(),
corpo: Joi.string().necessário(),
}
}
}
}
Retornaremos a postagem criada.
Atualizar uma postagem PUT /api/v1/users/{userId}/posts/{postId}
Para atualizar uma postagem, fornecemos um objeto validado, como fizemos na criação, e permitiremos a alteração do título e do corpo da postagem. Aqui, vamos consultar a postagem usando o modelo Post e a função getById, portanto, quando recuperamos a postagem, verificamos se o usuário corresponde ao usuário fornecido no caminho. Se corresponder, atualizamos os campos na postagem com os valores da solicitação e salvamos a postagem atualizada.
{
método: 'PUT',
caminho: '/api/v1/users/{userId}/posts/{postId}',
configuração: {
manipulador: (solicitação, resposta) => {
Postar.getById(solicitação.parâmetros.postId, (erro, postagem) => {
se (erro) {
retorno resposta({
status: 400,
mensagem: erro.mensagem
}).código(400);
}
se (solicitação.parâmetros.userId === postagem.usuário._id) {
const carga útil = solicitação.carga útil;
se (carga útil.título) {
postagem.título = carga útil.título;
}
se (carga útil.corpo) {
postagem.corpo = carga útil.corpo;
}
postagem.salvar((erro) => {
se (erro) {
retorno resposta({
status: 400,
mensagem: erro.mensagem
}).código(400);
}
retorno resposta(postagem).código(200);
});
} mais {
retorno resposta({
status: 401,
mensagem: "O usuário não pode editar a postagem"
}).código(401);
}
})
},
validar: {
consulta: validateUser,
carga útil: {
título: Joi.string().necessário(),
corpo: Joi.string().necessário(),
}
}
}
}
Retornaremos a postagem atualizada. Se o usuário não corresponder ao usuário na publicação, receberemos um erro de autorização porque o usuário não é o proprietário da publicação.
Excluir uma postagem DELETE /api/v1/users/{userId}/posts/{postId}
Como fizemos na atualização, consultamos a postagem e verificamos se o usuário do caminho corresponde ao usuário da postagem. Se corresponderem, prosseguimos e excluímos a postagem.
{
método: 'DELETE',
caminho: '/api/v1/users/{userId}/posts/{postId}',
configuração: {
manipulador: (solicitação, resposta) => {
Postar.getById(solicitação.parâmetros.postId, (erro, postagem) => {
se (erro) {
retorno resposta({
status: 400,
mensagem: erro.mensagem
}).código(400);
}
se (solicitação.parâmetros.userId === postagem.usuário._id) {
postagem.remover((erro) => {
se (erro) {
retorno resposta({
status: 400,
mensagem: erro.mensagem
}).código(400);
}
retorno resposta(postagem).código(200);
});
} mais {
retorno resposta({
status: 401,
mensagem: "O usuário não pode excluir a postagem"
}).código(401);
}
})
},
validar: {
consulta: validateUser
}
}
}
Devolvemos a postagem excluída.
Por fim, exportamos as rotas.
módulo.exportações = rotas;
Teste
Para testar a API, podemos fazer isso com o Postman, cURL ou qualquer outro aplicativo.
Abaixo, criamos alguns exemplos de cURL para testar a API. Os IDs usados são os que criamos com as operações POST, portanto, ao executá-los, lembre-se de alterar o caminho para corresponder aos IDs dos recursos que você gerou.
# consultamos os usuários
enrolar –X GET "http://localhost:5000/api/v1/users"
# criamos um usuário
enrolar –X POST –H "Content-Type: application/json" –d ‘{
"name": "jose",
"senha": "jose",
"email": "jose.navarro@famoco.com"
}’ "http://localhost:5000/api/v1/users"
# devemos obter um json com um usuário
enrolar –X GET "http://localhost:5000/api/v1/users"
#, devemos obter os usuários com esse ID
enrolar –X GET “http://localhost:5000/api/v1/users/e0b66baa-851d-4aae-9ef2-f12575519e5e”
# atualizamos o usuário
enrolar –X PUT –H "Content-Type: application/json" –d ‘{
"name": "jose_update",
"senha": "joseedit"
}’ “http://localhost:5000/api/v1/users/e0b66baa-851d-4aae-9ef2-f12575519e5e”
# excluímos o usuário
enrolar –X DELETE “http://localhost:5000/api/v1/users/e0b66baa-851d-4aae-9ef2-f12575519e5e”
Mensagens #
# consultamos a postagem de um usuário
enrolar –X GET “http://localhost:5000/api/v1/users/e717b7a3-e991-441e-8bca-562f2a572b19/posts”
# criamos uma postagem
enrolar –X POST –H "Content-Type: application/json" –d ‘{
"title": "my post title",
"body": "my post body"
}’ “http://localhost:5000/api/v1/users/e717b7a3-e991-441e-8bca-562f2a572b19/posts”
# consultamos para uma postagem
enrolar –X GET “http://localhost:5000/api/v1/users/e717b7a3-e991-441e-8bca-562f2a572b19/posts/94b1dd8e-73aa-4e4e-8b29-0870e6515945”
# atualizamos uma postagem
enrolar –X PUT –H "Content-Type: application/json" –d ‘{
"title": "meu título editado",
"body": "my edited body" (meu corpo editado)
}’ “http://localhost:5000/api/v1/users/e717b7a3-e991-441e-8bca-562f2a572b19/posts/94b1dd8e-73aa-4e4e-8b29-0870e6515945”
# excluímos uma postagem
enrolar –X DELETE “http://localhost:5000/api/v1/users/e717b7a3-e991-441e-8bca-562f2a572b19/posts/94b1dd8e-73aa-4e4e-8b29-0870e6515945”
Conclusão
Como vimos, foi fácil desenvolver uma API REST básica para executar uma operação CRUD, e o código é simples e fácil de ler. E com o Ottoman, conseguimos abstrair a lógica do banco de dados para trabalhar com objetos e com os métodos que o ODM nos forneceu.