Backend
Denna veckan tittar vi på hur vi kan skapa ett API som svarar med JSON med hjälp av Express och en dokument-orienterad databas. Databasen vi ska använda heter mongodb och är av typen NoSQL.
Läsa
Vi vänder oss till dokumentationen för Node och Express för att ytterligare se hur vi kan skapa ett API med Express.
Bekanta dig översiktligt med organisationen kring databasen MongoDB. Övningen (längre ned) kommer vidare utgå från informationen på denna webbplatsen.
Bekanta dig översiktligt med dokumentationen för “MongoDB Node.js driver” vilken är den driver vi kommer använda för att koppla JavaScript i Node.js till MongoDB. Det handlar både om referens-dokumentationen och API-dokumentationen. Användarexemplen är ett bra ställe att börja.
Titta
Vi ska denna veckan skriva en del asynkron kod och det kan vara bra att ha lite extra bra koll på hur “Event-loop” fungerar i JavaScript. Denna video ger en bra introduktion till hur det fungerar både för frontend och backend.
Sen låter vi Chief Technical Officer Eliot Horowitz hos MongoDB berätta om Dokumentorienterade databaser.
Path of Least Resistance
I detta kursmoment är det inte lika många val som förra kursmomentet. Det som kan underlätta är att använda sig av exempelkoden nedan och materialet i denna artikeln.
I stycket En liten titt på frontend kommer några exempel på hur man kan använda useEffect
och useState
i React för att hämta dokument samt uppdatera innehåll i text editorn.
Exempelkod
Om ni vill titta på ett fullständigt exempelprogram som använder alla dessa tekniker är auth_mongo ett bra ställe att börja. auth_mongo repot är en klon av det auth repo som användes i projektet i kursen webapp. Jag har bytt databasen från SQLite till mongodb.
Material
Modulen Express finns på npm. Express är en del av MEAN som är en samling moduler för att bygga webbapplikationer med Node.js. I denna artikeln kommer vi att använda MongoDB (M), Express (E), Node.js (N) i MEAN. A står för Angular, men kan bytas ut mot ett annat frontend ramverk. Dock blir förkortningarna något sämre (MERN, MESN, MEVN…).
Innan vi börjar så skapar vi en package.json
som kan spara information om de moduler vi nu skall använda.
# Ställ dig i katalogen du vill jobba
$npm init
När du ombeds döpa paketet så ange “editor-backend” eller något liknande (det spelar ingen roll). Använd bara inte “express” eftersom det paketnamnet redan finns och du får problem i nästa steg. Du kan köra om npm init
om du vill ändra namn, eller redigera namnet direkt i filen package.json
.
Nu kan vi installera paketen vi skall använda så här från början express
, cors
och morgan
. Vi väljer att spara dem i vår package.json
.
$npm install express cors morgan --save
Vi använder oss av cors
för att hantera Cross-Origin Sharing problematik och morgan
för loggning av händelser i API:t.
Då vi inte vill ha node_modules
katalogen versionshanterad i git skapar vi filen .gitignore
och lägger “node_modules/” som första rad i den filen.
Verifiera att Express fungerar
Låt oss starta upp en server för att se att installationen gick bra.
Jag börjar med kod som startar upp servern tillsammans med en route för /
och sparar i en fil du själv skapar app.js
.
const express = require("express");
const app = express();
const port = 1337;
// Add a route
app.get("/", (req, res) => {
res.send("Hello World");
});
// Start up server
app.listen(port, () => console.log(`Example API listening on port ${port}!`));
Sedan startar jag servern.
$node app.js
Example API listening on port 1337!
Nu kan jag skicka requester till servern via curl.
$curl localhost:1337
Hello World
Om jag använder en route som inte finns så får jag en 404 tillsammans med ett svar som säger att routen inte finns.
$curl -i localhost:1337/asd
HTTP/1.1 404 Not Found
X-Powered-By: Express
Content-Security-Policy: default-src 'self'
X-Content-Type-Options: nosniff
Content-Type: text/html; charset=utf-8
Content-Length: 134
Date: Wed, 15 Mar 2017 08:47:43 GMT
Connection: keep-alive
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Cannot GET /asd</pre>
</body>
Pröva nu samma routes via din webbläsare. Du bör få motsvarande svar även i din webbläsare.
Det verkar som allt gick bra och Express är uppe och snurrar och svarar på tilltal.
Låt npm köra dina skript
I filen package.json
kan du lägga in skript och köra dem via npm
. Du kan till exempel lägga till skriptet för att starta servern så här.
{
"scripts": {
"start": "node app.js"
}
}
Nu kan du starta servern via npm start
. Det blir ett sätt att samla enklare skript in i din package.json
.
Svara med JSON
I de allra flesta fall vill vi att vårt API svarar med ett JSON svar. För det använder vi response
objektets inbyggda funktion json
istället för send
som vi såg ovan.
const express = require("express");
const app = express();
const port = 1337;
// Add a route
app.get("/", (req, res) => {
const data = {
data: {
msg: "Hello World"
}
};
res.json(data);
});
// Start up server
app.listen(port, () => console.log(`Example API listening on port ${port}!`));
I exemplet ovan skickar vi ett JSON objekt när vi skickar en förfrågan till /
. Vi startar om servern och vi får följande svar om vi testar med curl i terminalen.
$curl localhost:1337
{"data":{"msg":"Hello World"}}
Automatisk omstart av node-appen
Vid det har laget har du nog redan börjat tröttna på att starta om din server varje gång du har ändrat i koden, så låt oss göra nått åt detta. Vi använder oss av npm modulen nodemon
(Dokumentation) för att starta om vår node applikation varje gång vi sparar. Vi installerar nodemon
som ett globalt paket, så vi kan använda det för alla vår node applikationer.
$npm install -g nodemon
För att starta vår applikation i nodemon kontext ändrar vi vårt npm start
skript.
{
"scripts": {
"start": "nodemon app.js"
}
}
Routing mot olika request metoder
En route sätts upp för att svara mot en speciell request metod såsom GET, POST, PUT, DELETE. Det är på det sättet man bygger upp en RESTful tjänst.
Här är fyra routes som har samma url, men skiftar i requestens metod.
// Testing routes with method
app.get("/user", (req, res) => {
res.json({
data: {
msg: "Got a GET request"
}
});
});
app.post("/user", (req, res) => {
res.json({
data: {
msg: "Got a POST request"
}
});
});
app.put("/user", (req, res) => {
res.json({
data: {
msg: "Got a PUT request"
}
});
});
app.delete("/user", (req, res) => {
res.json({
data: {
msg: "Got a DELETE request"
}
});
});
Om du testar med din webbläsare så blir det en GET request.
För att testa de andra metoderna så använder jag verktygen Postman eller RESTClient som är ett plugin till Firefox. Med de verktygen kan jag välja om jag skall skicka en GET, POST, PUT, DELETE eller någon annan av de HTTP-metoder som finns. En sådan REST-klient är ett värdefullt utvecklingsverktyg.
Så här ser det ut när jag skickar en request med en annan metod än GET.
Det var routes och stöd för olika metoder det. Se till att du installerar en klient motsvarande Postman eller RESTClient och testa din egen server.
Man vill ofta skicka en annan statuskod än 200 när man gör andra typer av requests än GET. Det kan vi göra med response
objektets inbyggda funktion status
.
// Testing routes with method
app.get("/user", (req, res) => {
res.json({
data: {
msg: "Got a GET request, sending back default 200"
}
});
});
app.post("/user", (req, res) => {
res.status(201).json({
data: {
msg: "Got a POST request, sending back 201 Created"
}
});
});
app.put("/user", (req, res) => {
// PUT requests should return 204 No Content
res.status(204).send();
});
app.delete("/user", (req, res) => {
// DELETE requests should return 204 No Content
res.status(204).send();
});
Vi skickar alltså tillbaka statusen 201 när vi skapar objekt med POST anrop och 204 när vi uppdaterar eller tar bort. Det är enkelt gjort med status
funktion. Innebörden av alla HTTP status koder finns i följande lista.
Route med dynamiskt innehåll
Vi skapar nya routes för att se hur routern hanterar dynamiskt innehåll i form av parametrar.
const express = require("express");
const app = express();
const port = 1337;
// Add a route
app.get("/", (req, res) => {
const data = {
data: {
msg: "Hello World"
}
};
res.json(data);
});
app.get("/hello/:msg", (req, res) => {
const data = {
data: {
msg: req.params.msg
}
};
res.json(data);
});
// Start up server
app.listen(port, () => console.log(`Example API listening on port ${port}!`));
Vi kan nu använda följande routes och se vad som händer.
/
/hello/Hello-World
/hello/Hello World
/hello/Jag kan svenska ÅÄÖ
Vi ser att parametern hanteras och kan nås i routen via req.params
. Vi ser också att mellanslag och svenska tecken hanteras och översätts med encodeURIComponent()
.
I webbsidan ser det ut som det ska.
I terminalen där servern kör ser det ut så här.
GET
/hello/Jag%20kan%20svenska%20%C3%85%C3%84%C3%96
Det vi ser är exempel på hur webbläsaren och servern hanterar encodning av udda tecken.
Webbläsaren konverterar länken, urlencodar, så att mellanslagen byts ut mot %20
. När länken tas emot som en route och översätts till parametrar, så gör Express en urldecode på innehållet. Detta är sättet som används för att hantera udda tecken i en webblänk och det sker automatiskt av webbläsaren och Express.
Det fungerar så här, om man översätter det till ren JavaScript.
$node
> a = encodeURIComponent("Jag kan svenska åäö")
'Jag%20kan%20svenska%20%C3%A5%C3%A4%C3%B6'
> b = decodeURIComponent(a)
'Jag kan svenska åäö'
>
Det är bra att veta om att det finns en hantering av udda tecken som sker i bakgrunden.
Om man vill använda sig av parametrar tillsammans med HTTP metoderna POST, PUT och DELETE används body-parser
. Vi importerar modulen längst upp i app.js
. Och lägger sen till att vi vill göra en parse på bodyn genom följande rader kod.
const bodyParser = require("body-parser");
...
app.use(bodyParser.json()); // for parsing application/json
app.use(bodyParser.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded
Ett annat sätt att uppnå samma funktionalitet finns numer tillgängligt direkt i express.
app.use(express.json());
Middleware - CORS och loggning
I express finns termen “middleware” som benämning på callbacks som anropas innan själva routens hanterare anropas. En middleware kan också vara en hanterare som alltid anropas för alla routes.
Låt oss skapa en sådan middleware, som alltid anropas, oavsett route. Den skall skriva ut vilken route som accessades och med vilken metod.
Vi lägger till middleware via metoden app.use()
. Vi kan lägga till dem för en specifik route, eller för alla routes.
// This is middleware called for all routes.
// Middleware takes three parameters.
app.use((req, res, next) => {
console.log(req.method);
console.log(req.path);
next();
});
Middleware anropas i den ordningen de är definierade, när de matchar en route. Använd ett anrop till next()
när du är klar och vill skicka vidare kontrollen till nästa middleware och slutligen till routens hanterare.
Om du vill att denna middleware alltid skall anropas så behöver du lägga den högst upp i din kod.
På serversidan ser du nu delar av innehållet i request-objektet som visar metoden och pathen som anropats, samt eventuellt inkommande parametrar.
Loggning med tredjepartsmodul
Vi väljer i vårt API att använda en tredje parts modul morgan
för loggning. Vi har redan installerad morgan
som en del av node_modules
och vi lägger till modulen i app.js
enligt nedan och använder den inbyggda middleware för att skriva ut loggen. Vi lägger in anropet till morgan
innan vi anropar några routes då vi vill att loggningen sker för alla routes.
const express = require('express');
const morgan = require('morgan');
const app = express();
const port = 1337;
// don't show the log when it is test
if (process.env.NODE_ENV !== 'test') {
// use morgan to log at command line
app.use(morgan('combined')); // 'combined' outputs the Apache style LOGs
}
Cross-Origin Resource Sharing (CORS)
Då vi vill att vårt API ska kunna konsumeras av många olika klienter vill vi tillåta att klienter från andra domäner kan hämta information från vårt API. Vi gör även detta med en tredjepartsmodul cors
, som vi installerade i början av artikeln. På samma sätt som för morgan
använder vi den inbyggda middleware och använder funktionen use
.
const express = require('express');
const cors = require('cors');
const morgan = require('morgan');
const app = express();
const port = 1337;
app.use(cors());
// don't show the log when it is test
if (process.env.NODE_ENV !== 'test') {
// use morgan to log at command line
app.use(morgan('combined')); // 'combined' outputs the Apache style LOGs
}
404 med routes
När användaren försöker nå en route som inte finns så blir det ett svar med statuskod 404.
Man kan lägga till en egen route som blir en “catch all” och agerar kontrollerad hantering av 404.
// Add routes for 404 and error handling
// Catch 404 and forward to error handler
// Put this last
app.use((req, res, next) => {
var err = new Error("Not Found");
err.status = 404;
next(err);
});
Ovan så använder min hanterare för 404 den inbyggda felhanteraren. Det sker via anropet next(err)
där err
är ett objekt av typen Error
. Min variant är alltså att säga att nu är det felkod 404 och jag överlämnar till den inbyggda felhanteraren att skriva ut felmeddelandet.
Det finns alltså en inbyggd felhanterare som visar upp information om felet, tillsammans med en stacktrace. Det är användbart när man utvecklar.
När node startar upp Express så är det default i utvecklingsläge. Du kan testa att starta upp i produktionsläge, det ger mindre information i felmeddelandena.
$NODE_ENV="production" node app.js
Nu försvann stacktracen från klienten, men den syns fortfarande i terminalen där servern körs.
Vi ser till att även skapa ett npm skript för att köra i produktion som vi sedan kan använda på servern. Vi kan då köra npm run production
för att starta i i produktion.
{
"scripts": {
"start": "node app.js",
"production": "NODE_ENV='production' node app.js"
}
}
När vi utvecklar så blir det enklast att köra development läge (standard). Men när man sätter en server i produktion så får man se till att det också är produktionsläge för felmeddelandena, vilket innebär att visa så lite information som möjligt.
En egen hanterare för felutskrift
Vi kan skapa vår egen felhanterare och skicka felmeddelandet som JSON.
En egen felhanterare i Express kan se ut som det app.use
funktionsanrop längst ner. Vi kombinerar det med vår hanterare för 404 felmeddelande och använder next(err);
för att skicka vidare felmeddelandet till vår egen hanterare.
app.use((req, res, next) => {
var err = new Error("Not Found");
err.status = 404;
next(err);
});
app.use((err, req, res, next) => {
if (res.headersSent) {
return next(err);
}
res.status(err.status || 500).json({
"errors": [
{
"status": err.status,
"title": err.message,
"detail": err.message
}
]
});
});
Kom ihåg att en sådan här felhanterare är som all annan middleware och det är viktigt i vilken ordning de ligger. De kan anropas i den ordningen som de definieras.
Uppdelning av routes
Med tanke på de få routes vi kommer ha tillgängliga i våra API:er hade det inte varit helt orimligt att ha all hantering i app.js
, men vi väljer ändå att dela upp våra routes då vi gillar bra struktur inför framtida uppskalningar.
Vi skapar katalogen routes
och i den katalogen skapar vi två stycken filer index.js
och hello.js
. Här skapar vi och returnerar ett objekt av typen express.Router()
.
var express = require('express');
var router = express.Router();
router.get('/', function(req, res, next) {
const data = {
data: {
msg: "Hello World"
}
};
res.json(data);
});
module.exports = router;
Vi importerar sedan dessa filer i app.js
och använder de som routehanterare med ett funktionsanrop till use
.
...
const index = require('./routes/index');
const hello = require('./routes/hello');
...
app.use('/', index);
app.use('/hello', hello);
På det sättet håller vi app.js
liten i storlek och var sak har sin plats.
MongoDB som databas
I denna artikel installerar vi MongoDB lokalt på din utvecklingsdator, om du vill och har möjlighet kan du använda MongoDB i Docker. Artikeln MongoDB i Docker visar hur det kan gå till.
Vi kommer sedan använda oss av MongoDB Atlas för att driftsätta vår databas, men mer om det senare.
I kursrepot finns exempelkod under db/mongodb.
Om du vill lära dig mer om mongodb utanför kursen är MongoDB University en bra resurs.
Installation MongoDB
Windows
Gå till MongoDB Community Server och välj ditt operativsystem i listan. Följ sedan installationsinstruktionerna.
MacOS
Installera med hjälp av pakethanteraren brew med kommandona:
$brew tap mongodb/brew
$brew install mongodb-community@5.0
Starta sedan mongodb som en service med kommandot: brew services start mongodb-community@5.0
.
Linux (Debian/Ubuntu)
Vi börjar med att installera dirmngr
, för att kunna ta hand gpg nycklar, med kommandot sudo apt-get install dirmngr
. Vi följer sedan de rekommenderade installationsinstruktionerna hos MongoDB. Se till att välja rätt operativsystem i menyn.
Starta klienten
Det ska nu gå att starta mongodb klienten med kommandot mongosh
i din terminal. Kommandot help
inne i mongodb klienten ger en översikt över tillgängliga kommandon.
$mongosh
> help
db.help() help on db methods
db.mycoll.help() help on collection methods
sh.help() sharding helpers
rs.help() replica set helpers
help admin administrative help
help connect connecting to a db help
help keys key shortcuts
help misc misc things to know
help mr mapreduce
show dbs show database names
show collections show collections in current database
show users show users in current database
show profile show most recent system.profile entries with time >= 1ms
show logs show the accessible logger names
show log [name] prints out the last segment of log in memory, 'global' is default
use <db_name> set current database
db.foo.find() list objects in collection foo
db.foo.find( { a : 1 } ) list objects in foo where a == 1
it result of the last line evaluated; use to further iterate
DBQuery.shellBatchSize = x set default number of items to display on shell
exit quit the mongo shell
Du som är van vid liknande klienter till andra databaser kan känna igen dig bland de kommandon som erbjuds.
Det finns en manual som hjälper dig igång med grunderna och baskommandona.
Skapa en databas
Vi prövar att använda klienten för att skapa en databas och lägga in ett dokument i en collection.
Först skapar vi databasen.
> use mumin
> show collections
Den är tom och innehåller inga collections ännu. Vi skapar en collection genom att lägga ett dokument i den.
> db.crowd.insertOne( { name: "Mumintrollet" } )
{
"acknowledged" : true,
"insertedId" : ObjectId("5a13069000b2ff0b912aeeb6")
}
> show collections
crowd
Om jag fyller på ytterligare några dokument så kan det se ut så här när vi frågar efter innehållet i en collection.
> db.crowd.find()
{ "_id" : ObjectId("5a13069000b2ff0b912aeeb6"), "name" : "Mumintrollet" }
{ "_id" : ObjectId("5a13079100b2ff0b912aeeb7"), "name" : "Sniff" }
{ "_id" : ObjectId("5a13079b00b2ff0b912aeeb8"), "name" : "Snusmumriken" }
{ "_id" : ObjectId("5a1307a900b2ff0b912aeeb9"), "name" : "Snorkfröken" }
Vi kan uppdatera samtliga dokument/objekt i vår collection.
> db.crowd.updateMany({}, {$set: { bor: "Mumindalen" }})
{ "acknowledged" : true, "matchedCount" : 4, "modifiedCount" : 4 }
> db.crowd.find().pretty()
{
"_id" : ObjectId("5a13069000b2ff0b912aeeb6"),
"name" : "Mumintrollet",
"bor" : "Mumindalen"
}
{
"_id" : ObjectId("5a13079100b2ff0b912aeeb7"),
"name" : "Sniff",
"bor" : "Mumindalen"
}
{
"_id" : ObjectId("5a13079b00b2ff0b912aeeb8"),
"name" : "Snusmumriken",
"bor" : "Mumindalen"
}
{
"_id" : ObjectId("5a1307a900b2ff0b912aeeb9"),
"name" : "Snorkfröken",
"bor" : "Mumindalen"
}
>
Det finns alltså ett antal vanliga CRUD-operationer vi kan göra för att jobba med datat i databasen. Du kan läsa mer om dessa CRUD-operationer i manualen.
Låt oss gå vidare och skapa ett program som använder vår nyskapade databas.
Node till MongoDB
Först installerar vi npm-paketet mongodb
som är en Node driver till databasen MongoDB. Det finns redan i package.json
så följande kommandon fungerar.
npm install
npm install mongodb --save
Vi kan läsa om MongoBD Node.JS Driver i dokumentationen. Där finner vi också dokumentationen för API:et och dess metoder.
Setup med grunddata
I filen src/setup.js
finns kod som kopplar upp sig mot MongoDB och skapar databasen mumin, rensar den från innehåll och lägger in en del av befolkningen från mumindalen i en collection crowd
genom att hämta data från filen src/setup.json
.
Du kan pröva köra programmet och därefter koppla dig med klienten mongo för att se att datan ligger på plats.
$node src/setup.js
$mongo --eval "db.crowd.find().pretty()"
MongoDB shell version v3.4.10
connecting to: mongodb://mongodb/mumin
MongoDB server version: 3.4.10
{
"_id" : ObjectId("5a134ec3c28e762f068f48f1"),
"name" : "Mumintrollet",
"bor" : "Mumindalen"
}
{
"_id" : ObjectId("5a134ec3c28e762f068f48f2"),
"name" : "Sniff",
"bor" : "Mumindalen"
}
{
"_id" : ObjectId("5a134ec3c28e762f068f48f3"),
"name" : "Snusmumriken",
"bor" : "Mumindalen"
}
{
"_id" : ObjectId("5a134ec3c28e762f068f48f4"),
"name" : "Snorkfröken",
"bor" : "Mumindalen"
}
Söka information från databasen
I filen src/search.js
finns kod som kopplar upp sig mot MongoDB och söker i databasen. Kodexemplet visar på ett par alternativa sätt att jobba med MongoDB avseende den asynkrona biten. Det API som erbjuds bygger på att man kan välja callbacks eller Promise för att hantera det asynkrona flödet.
Låt oss titta på koden.
Först har vi en funktion som kopplar sig mot databasen och en colletion samt utför själva find-operationen.
/**
* Find documents in an collection by matching search criteria.
*
* @async
*
* @param {string} dsn DSN to connect to database.
* @param {string} colName Name of collection.
* @param {object} criteria Search criteria.
* @param {object} projection What to project in results.
* @param {number} limit Limit the number of documents to retrieve.
*
* @throws Error when database operation fails.
*
* @return {Promise<array>} The resultset as an array.
*/
async function findInCollection(dsn, colName, criteria, projection, limit) {
const client = await mongo.connect(dsn);
const db = await client.db();
const col = await db.collection(colName);
const res = await col.find(criteria, projection).limit(limit).toArray();
await client.close();
return res;
}
Funktionen använder konstruktionen async/await för att serialisera flödet mot databasen. Varje metod som jobbar mot databasen, i exemplet ovan, är asynkron och har alternativet att använda callbacks, eller Promise. I koden ovan bygger vi på att ett Promise returneras när respektive metod är avklarad.
En vanlig frågeställning i en async funktion är om await behövs eller inte. För att besvara det behöver man delvis veta om metoden/funktionen returnerar ett Promise eller ej. Här vänder vi oss till API-manualen för respektive metod. Man kommer inte framåt utan att bli bekant med det API man jobbar med. En vanlig fråga är till exempel vad som returneras inom ett Promise, vilka argument man har tillgång till. API manualen ger svaret.
Låt oss titta på hur vi kan använda funktionen ovan. Jag kan visa två alternativ och vi börjar med async/await.
Jag lägger premisserna för sökningen i variabler, för tydlighetens skull.
// Find documents where namn starts with string
const criteria2 = {
namn: /^Sn/
};
const projection2 = {
_id: 1,
namn: 1
};
const limit2 = 3;
Sedan wrappar jag koden i en Immediately Invoked Async Arrow Function för att kunna använda await inom funktionen.
// Do it within an Immediately Invoked Async Arrow Function.
// This is to enable usage of await within the function scope.
(async () => {
// Find using await
try {
let res = await findInCollection(
dsn, "crowd", criteria2, projection2, limit2
);
console.log(res);
} catch(err) {
console.log(err);
}
})();
Jag lägger koden inom en traditionell try/catch för att hantera eventuella fel som uppkommer. Jag använder await på findInCollection()
och lägger svaret i en variabel. På det sättet löser jag serialiseringen.
Vi tittar på en annan variant.
(() => {
// Find using .then()
findInCollection(dsn, "crowd", criteria1, projection1, limit1)
.then(res => console.log(res))
.catch(err => console.log(err));
})();
Här finns inget krav på att använda async, ej heller att wrappa koden inom ett funktionsscope. Serialiseringen sköts av .then()
och felhanteringen i .catch()
.
Vi hade också kunnat tänka oss en variant av findInCollection()
som jobbar med callbacks. Funkionen hade isåfall tagit ytterligare ett argument callback
som hade anropats när funktionen var klar.
Lägga till och uppdatera data
Vi har i ovanstående sett hur vi läser data från databasen och i exempelkoden db/mongodb/src/setup.js
finns ett exempel där funktionen insertMany används.
Förutom insertMany
finns insertOne
funktionen där man lägger till ett dokument i databasen. Om vi vill lägga till ett dokument med attributen name
och html
gör vi på följande sätt.
const doc = {
name: body.name,
html: body.html,
};
const result = await db.collection.insertOne(doc);
MongoDB lägger automatiskt till ett _id
fält i dokumentet/objektet och vi kan kolla om allt gått bra och titta på det objekt vi har lagt till med följande kod. result.ops
innehåller det objekt som har lagts till i databasen bland annat det automatgenererade _id
.
if (result.result.ok) {
return res.status(201).json({ data: result.ops });
}
Detta _id
behövs sedan när vi vill uppdatera dokumentet i databasen. Vi gör det med funktionen updateOne. Först importerar vi ObjectId funktionen för att kunna hitta rätt _id
i databasen. Vi skapar sedan ett filter
och ett updateDocument
och använder oss av updateOne
. Bara de fält som skickas in uppdateras, vill vi ersätta dokumentet istället kan vi använda replaceOne
.
const ObjectId = require('mongodb').ObjectId;
const filter = { _id: ObjectId(body["_id"]) };
const updateDocument = {
name: body.name,
html: body.html,
};
const result = await db.collection.updateOne(
filter,
updateDocument,
);
Express till MongoDB
Hur kan det se ut om vi kopplar in databasen MongoDB mot en instans av Express? Låt oss titta på ett exempel i src/server.js
som exponerar en route /list
som visar allt innehåll i en collection i databasen.
Vi kan starta upp server och testa att accessa routen.
$npm start
Server is listening on 1337
Via en webbläsare eller curl kan vi nu komma åt routen och med kommadnot jq får vi en renare utskift.
$curl -s http://localhost:1337/list | jq
[
{
"_id": "5a13efb54dbe18550bce601b",
"namn": "Mumintrollet",
"bor": "Mumindalen"
},
{
"_id": "5a13efb54dbe18550bce601c",
"namn": "Sniff",
"bor": "Mumindalen"
},
{
"_id": "5a13efb54dbe18550bce601d",
"namn": "Snusmumriken",
"bor": "Mumindalen"
},
{
"_id": "5a13efb54dbe18550bce601e",
"namn": "Snorkfröken",
"bor": "Mumindalen"
}
]
Som vi kunde ana var det inget större bekymmer att flytta in vår kod i en route i express som stödjer async funktioner som callbacks till en route.
Bryta ut databas koden
För att få mer DRY kod och för att underlätta för testning lite längre fram i kursen är detta ett bra tillfälle att bryta ut hanteringen av databas uppkopplingen till en egen modul. Jag har i mitt projekt skapat en katalog db
där jag har lagt filen database.js
. Här skapar jag först en dsn
sträng. Om vi håller på att testa koden ändrar jag den till en test databas istället, mer om detta när vi senare i kursen ska testa vår applikation.
Nästa steg är som vi tidigare har gjort i exempelprogrammen att koppla oss mot databasen och som det sista steget att returnera client
och collection
.
const mongo = require("mongodb").MongoClient;
const config = require("./config.json");
const collectionName = "docs";
const database = {
getDb: async function getDb () {
let dsn = `mongodb://localhost:27017/folinodocs`;
if (process.env.NODE_ENV === 'test') {
dsn = "mongodb://localhost:27017/test";
}
const client = await mongo.connect(dsn, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
const db = await client.db();
const collection = await db.collection(collectionName);
return {
collection: collection,
client: client,
};
}
};
module.exports = database;
Vi kan sedan i koden hämta databasen, ställa frågor och sedan stänga ner databasen.
const db = await database.getDb();
const resultSet = await db.collection.find({}).toArray();
await db.client.close();
Felhantering av frågor till databasen
Vi har ovan sett en kort introduktion till felhantering. Och här kommer ett lite längre exempel där vi även tittar på hur vi kan stänga ner databasen. Vi använder oss av konstruktionen try-catch-finally
(Dokumentation).
let db;
try {
db = await database.getDb();
const filter = { email: email };
const keyObject = await db.collection.findOne(filter);
if (keyObject) {
return res.json({ data: keyObject });
}
} catch (e) {
return res.status(500).json({
errors: {
status: 500,
source: "/",
title: "Database error",
detail: e.message
}
});
} finally {
await db.client.close();
}
Finally delen av konstuktionen utförs alltid både när det har gått bra och vid fel.
En liten titt på frontend
Innan vi dyker ner i att driftsätta både databas och backend tar vi en liten titt på frontend. Tanken med kursen är att ni ska utforska ramverket till stor del på egen hand, men i och med introduktionen av Path of Least Resistance kommer här lite hjälp på vägen.
I vår frontend vill vi kunna ladda in befintliga dokument, vi vill kunna skapa ett nytt dokument, vi vill kunna välja ett dokument och när vi har ändrat i valt dokument vill vi kunna uppdatera dokumentet. Vi kommer gå igenom en del av dessa funktioner i nedanstående.
Jag kommer i nedanstående utgå ifrån att vi har delat koden upp i modeller och komponenter. Så jag har två filer i frontend som vi kommer fokusera på models/docs.js
och components/editor.js
.
Hämta alla dokument
Låt oss börja med att hämta alla dokument från backend och visa de i en dropdown. Jag kommer utgå från att jag har en GET /docs
route i backend som skickar tillbaka alla dokument. Jag börjar med att importera modellen genom följande kod: import docsModel from '../models/docs';
. Sedan skapar jag en docs
array som blir en state-variabel. useEffect
används för att hämta dokumenten och docs
-arrayen setts till det som returneras.
const [docs, setDocs] = useState([]);
useEffect(() => {
(async () => {
const allDocs = await docsModel.getAllDocs();
setDocs(allDocs);
})();
}, []);
I docsModel
har jag följande kod som hämtar och returnerar data från backend:
const docs = {
getAllDocs: async function getAllDocs() {
const response = await fetch(`${URL}/docs`);
const result = await response.json();
return result.data;
},
};
export default docs;
Vi kan nu i vår komponent rita upp ett select
element med innehållet från backend.
<select
onChange={fetchDoc}
>
<option value="-99" key="0">Choose a document</option>
{docs.map((doc, index) => <option value={index} key={index}>{doc.name}</option>)}
</select>
Så långt så gott.
Hämta baserat på en annan variabel
Dock vill vi utöka funktionaliteten lite grann. Vi vill hämta alla dokumenten varje gång vi ändrar dokumentet vi skriver i. Det underlättar till exempel när vi vill skapa nya dokument.
Vi kan göra det genom att lägga till en variabel som en del av det andra argumentet till useEffect
. Varje gång variabeln ändras kommer funktionen inuti useEffect
köras.
const [docs, setDocs] = useState([]);
const [currentDoc, setCurrentDoc] = useState({});
useEffect(() => {
(async () => {
const allDocs = await docsModel.getAllDocs();
setDocs(allDocs);
})();
}, [currentDoc]);
Driftsättning
Molnet eller the cloud har under de senaste 10 åren växt fram enormt fort. Om du vill ha en kort introduktion till molnet kan Bill Laberis’ bok “What is the cloud?” rekommenderas. Är inte nödvändigt för att klara kursen, men är snabbläst. Du kommer åt boken via biblioteket på BTH och välj O’reilly. Du ska nu kunna söka på “What is the cloud?” i Sökrutan och första träffen bör vara “What is the cloud?”.
MongoDB
Vi börjar med att skaffa oss en plats för vår mongodb databas. Vi kommer i kursen använda oss av MongoDB Atlas. Atlas är en moln-tjänst för databaser.
Skapa en användare genom att klicka på “Start free”. Skapa sedan ett cluster genom knappen “Create a New Cluster”. Bilden nedan visar de tre olika cluster-typerna och välj här “Shared Clusters” då de är gratis.
Skapa sedan ett cluster där du väljer en cloud provider och en plats för vart servern ska stå. Se till att under Cluster Tier välja M0 som är gratis varianten.
Efter att vi skapat ett cluster vill vi skapa en användare som får komma åt clustret och de databaser som kommer skapas i clustret. Nedan väljer vi först Database Access och sedan Add New Database User.
Sedan skapar vi en användare för att koppla oss mot databaserna. Välj att vi vill använda lösenord som autentiseringsmetod.
För att skydda databasen ytterligare kan vi bestämma vilka IP-adresser som får komma åt databasen. Välj Network Access och sedan Add IP Adress.
Tryck på knappen Allow Access From Anywhere för att lägga till alla IP-adresser det kommer underlätta när vi har driftsatt backend. Blir lite sämre säkerhet, men vi är medvetna om detta och förstår innebörden.
Låt oss nu koppla upp vår applikation mot vår nya databas hos MongoDB atlas. Gå till Clusters fliken i mongodb atlas gränssnittet och tryck på Connect, välj sedan Connect Your application.
Välj sedan korrekt driver och version, senaste bör vara korrekt. Kopiera sedan in detta i din db/database.js
.
Jag väljer att använda mig av npm paketet dotenv
för att hantera användarnamn och lösenord till databasen. Vi installerar paketet genom att skriva npm install --save dotenv
. Vi kan sedan skapa en fil .env
i roten av vår backend-katalog. Jag har exkluderat .env
från Git genom att fylla i sökvägen till filen i repots .gitignore
-fil.
Vi nu fylla i användarnamn och lösenord i filen .env
:
ATLAS_USERNAME="YOUR_USERNAME"
ATLAS_PASSWORD="YOUR_PASSWORD"
Högst upp i din fil app.js
kan du nu köra anropet require('dotenv').config()
.
Vilket gör att jag kan skapa min dsn sträng på följande sätt.
let dsn = `mongodb+srv://${process.env.ATLAS_USERNAME}:${process.env.ATLAS_PASSWORD}@cluster0.hkfbt.mongodb.net/folinodocs?retryWrites=true&w=majority`;
Vi kan nu verifiera att kopplingen till mongodb atlas fungerar genom att köra igång vår backend lokalt och se att det fungerar som tidigare.
Express Appen
Med databasen på plats och vi har verifierad att det fungerar bra fortsätter vi med backend. Vi ska driftsätta vår Express/Node applikation i Microsofts Azure Cloud, men innan vi kommer så långt behöver vi uppdatera så cloudet kan ändra porten som vårt API lyssnar på.
Vi kan utnyttja process
, som är en global variabel med innehåll om den Node process som vi kör vårt API i. I den kan vi hämta ut miljövariabler och i detta fallet PORT
. Om vi ändrar till nedanstående kod får vi miljövariabelns värde om den är satt annars vår standard port 1337.
const port = process.env.PORT || 1337;
Nedanstående är en översiktlig genomgång av denna Microsoft Guide, så om du vill ha med alla detaljer välj guiden.
Vi kommer göra driftsättningen automagiskt via Visual Studio Code, så har du inte den installerad är det dags nu.
I Visual Studio Code installerar vi pluginen Azure App Services med hjälp av plugin menyn.
För att vi ska ha säker tillgång till våra konto på Azure är Multi-Factor Authentication (MFA) påslaget. Via Studentportalen finns det guider för hur man använder MFA. Se bilden under för att hitta guiderna.
Logga sedan in i Azure App Services genom att klicka på länken Sign in to Azure under den nya fliken Azure i din aktivitetsflik längst till vänster. Du ska logga in med din Studentkonto-mail: abcdxx@student.bth.se.
Se till att du har Code öppnat för din backend app och klicka sedan på lilla krysset bredvid App Service rubriken. Välj sedan den subscription som dyker upp. Första steget är sedan att välja ett unikt namn. Jag har valt jsramverk-editor-efostud som är mitt fejkade student-akronym så kan vara smart att använda jsramverk-editor-abcdxx.
Välj sedan stack för din app, senaste LTS är bästa valet.
Välj sedan prisnivå för appen. Ni ska kunna välja mellan Free och B1. Free kan vara lite trögt, så är bäst med B1.
Välj sedan Deploy i den dialog ruta som dyker upp. Nu sätter Code och Azure igång med att driftsätta din app. Efter en stund får du upp en länk med texten Browse Website och du kommer nu till din app.
Om du får problem med att inte ha rättigheter att skapa en server utanför North Europe kan meny-valet “Create New Web app (Advanced)” som nås via att högerklicka på prenumerationen under App Service i Code vara en bra lösning.
Kravspecifikation
Denna veckan är uppgiften uppdelat i två delar. En del handlar om backend och en del om hur din frontend applikation ska konsumera backend API:t.
Del 1: Backend
Skapa ett API för att kunna spara dokument från din editor.
Se till att det finns en
package.json
i katalogen. Filen skall innehålla alla beroenden som krävs.Skapa en
README.md
fil i ditt repo som beskriver hur man installerar moduler och startar ditt Me-API. Beskriv även hur du har valt att strukturera dina routes.Det ska gå att skapa (Create) och uppdatera (Update) dokumenter via din editor. Förslagsvis har ett dokument minst namn och innehåll förutom det automatgenererade _id.
Det ska gå att hämta alla dokument från API’t för att sedan kunna visas upp i din frontend.
Driftsätt din backend på Azure enligt beskrivningen ovan.
Committa alla filer och lägg till en tagg (1.0.0) med hjälp av
npm version 1.0.0
. Det skapas automatiskt en motsvarande tagg i ditt GitHub repo. Lägg till fler taggar efterhand som det behövs. Var noga med din commit-historik.Pusha upp repot till GitHub, inklusive taggarna.
Del 2: Frontend
Gör om din editor så att dokument kan skapas och hämtas in i editorn.
När ett befintligt dokument ändras ska det uppdateras istället för att skapas om på nytt.
Committa alla filer och lägg till en tagg (2.0.0) med hjälp av
npm version 2.0.0
. Det skapas automatiskt en motsvarande tagg i ditt GitHub repo. Lägg till fler taggar efterhand som det behövs. Var noga med din commit-historik.Pusha upp repot till GitHub, inklusive taggarna.
Länka till båda dina GitHub repon och den driftsatta klienten i en kommentar till din inlämning på Canvas.
Skriva
Vi fortsätter iterativt med att förbättra våra forskningsfrågor. Använd den återkopplingen du fick på första veckans frågor och förbättra frågorna.
Gå tillbaka till skrivguiden och titta under Syfte, problemformulering och forskningsfrågor – att begränsa ämne för bra tips.
Lämna in texten som PDF bilaga till din inlämning på Canvas.