Πριν ξεκινήσω να εργάζομαι σε αυτήν την πρώτη «μεγάλη εφαρμογή» είχα επικεντρωθεί μόνο στην ανάπτυξη κώδικα frontend. Μου αρέσει να δουλεύω με οπτικό χώρο και μου αρέσει η δυναμική οπτική ανατροφοδότηση που λαμβάνετε από την αντιμετώπιση προβλημάτων στο frontend, που απλά δεν λαμβάνετε όταν δουλεύετε βαθιά στα χαρακώματα του backend. Αλλά ως αποτέλεσμα, η κατανόησή μου για το backend ήταν σχεδόν ανύπαρκτη, ήταν ακριβώς αυτού του είδους το μαύρο κουτί που φτύνει δεδομένα. Δεν κατάλαβα καν πραγματικά τι ήταν το API! (ακόμη λιγότερο τι ήταν το REST API). Έτσι, αποδείχθηκε ότι η δημιουργία ενός αρκετά απλού backend χρησιμοποιώντας το Node και το Express.js ήταν μια από τις πιο ενημερωτικές πτυχές αυτής της προσπάθειας, και αυτό που πραγματικά εμπλούτισε την κατανόησή μου για το πώς λειτουργεί πραγματικά μια εφαρμογή Ιστού .

Για να ξεκινήσετε, τι είναι το back end; Το πίσω μέρος δημιουργεί έναν διακομιστή ο οποίος λαμβάνει εισερχόμενα αιτήματα (από το πρόγραμμα περιήγησης ή την εφαρμογή για κινητά, κ.λπ. στο frontend) και προβάλλει τις αντίστοιχες απαντήσεις. Για τους σκοπούς της εφαρμογής μας για αναπαραγωγή μουσικής, είναι βασικά ένα μεσαίο επίπεδο, μεταξύ της διεπαφής χρήστη και της βάσης δεδομένων μας, με όλη τη λογική που χρειαζόμαστε για να εξυπηρετήσουμε τα σωστά ερωτούμενα δεδομένα!

Τι είναι λοιπόν ένα API ή συγκεκριμένα τι είναι το REST API; Ένα REST API (το οποίο σημαίνει «Διεπαφή προγραμματισμού εφαρμογής μεταφοράς αντιπροσωπευτικής κατάστασης) είναι μάλλον περίπλοκος τρόπος για να πούμε έναν διακομιστή που ανταλλάσσει μόνο δεδομένα και όχι προβολές. Η αξία της χρήσης REST API είναι ότι εφόσον ανταλλάσσονται μόνο δεδομένα, το backend σας είναι εντελώς αγνωστικιστικό και μπορεί να εξυπηρετηθεί σε μια εφαρμογή ιστού ή εφαρμογή για κινητά, στην πραγματικότητα σε οποιαδήποτε διεπαφή χρήστη.

Εντάξει, ας ξεκινήσουμε.

Όπως αναφέρθηκε στην πρώτη μου δόση σε αυτήν τη σειρά, αυτό δεν προορίζεται να είναι ένα μάθημα βήμα προς βήμα, αλλά μάλλον ένα είδος αφηγηματικής πορείας για τη δημιουργία της πρώτης μεγάλης εφαρμογής σας. Αυτό συμβαίνει εν μέρει επειδή πιστεύω ότι τα σεμινάρια για μικρά πράγματα είναι υπέροχα, αλλά τα μαθήματα για μεγάλα πράγματα αποτελούν εμπόδιο στη μάθηση. ο καλύτερος τρόπος για να μάθεις προγραμματισμό είναι το χάος και να χτυπήσεις το κεφάλι σου στον τοίχο προσπαθώντας να λύσεις μερικά από τα μεγαλύτερα πράγματα. Αλλά ένα γενικό πλαίσιο εδώ και εκεί θα βοηθήσει τουλάχιστον τους νέους προγραμματιστές να χτυπήσουν το κεφάλι τους στον σωστό τοίχο. Για ένα λαμπρό μάθημα στο Node.js/Express.js (συμπεριλαμβανομένων των MVC, REST και GraphQL, καθώς και SQL και NoSQL) κατευθύνω όποιον ενδιαφέρεται σε αυτό το μάθημα, το οποίο με βοήθησε πάρα πολύ ενώ έκοβα δόντια στη δημιουργία του πρώτου μου API.

Προς το παρόν δεν θα βουτήξω βαθιά στη ρύθμιση των μοντέλων δεδομένων (ή συνδέσεων με βάσεις δεδομένων, τουλάχιστον όχι ακόμα), γιατί για κάθε εφαρμογή μπορούν να ρυθμιστούν διαφορετικά, και ούτως ή άλλως κάλυψα εν συντομία πώς σχηματοποιούσα τα δεδομένα μου στην πρώτη δόση πληρωμής. Επομένως, αυτό το άρθρο θα επικεντρωθεί κυρίως σε (1) τη ρύθμιση και την οργάνωση του αρχείου σας app.js και (2) ένα πλαίσιο για να σκεφτείτε τους ελεγκτές σας.

Μερικά χρήσιμα πακέτα και εξαρτήσειςπου ίσως θέλετε να χρησιμοποιήσετε στο API σας:

  • body-parser — εκτός αν θέλετε να αναλύσετε όλα τα εισερχόμενα αιτήματά σας από την αρχή, αυτό είναι ένα πολύ χρήσιμο εργαλείο και ειλικρινά δεν μπορώ να σκεφτώ έναν λόγο για να μην το χρησιμοποιήσω
  • express-validator — εάν σχεδιάζετε να εγγραφούν χρήστες, είναι καλύτερο να επικυρώσετε τόσο στο μπροστινό όσο και στο πίσω μέρος για να βεβαιωθείτε ότι πράγματα όπως οι κωδικοί πρόσβασης έχουν περισσότερους από 5 χαρακτήρες, τα μηνύματα ηλεκτρονικού ταχυδρομείου είναι email κ.λπ. Και αυτό το πακέτο θα εξοικονομήσει έχετε κάποια κωδικοποίηση Regex (που δεν είναι πάντα καλό;)
  • bcryptjs — ένα πακέτο για κρυπτογράφηση, ιδιαίτερα χρήσιμο εάν σκοπεύετε να συμπεριλάβετε εγγραφή χρήστη και να αποθηκεύσετε πληροφορίες χρήστη στη βάση δεδομένων σας, κάποιο είδος κρυπτογράφησης θα βοηθήσει στην ασφάλεια των κωδικών πρόσβασης χρήστη
  • nodemon — ως εξάρτηση προγραμματιστή, αυτό θα διευκολύνει πολύ την αντιμετώπιση προβλημάτων του API σας, καθώς διατηρεί τον διακομιστή σας ανανεωμένο

Οργάνωση του αρχείου σας app.js και ρύθμιση του Express

Εάν είστε εξοικειωμένοι με τη Javascript (η οποία είναι απαραίτητη προϋπόθεση για να κάνετε οτιδήποτε θα καλύψω… το ανέφερα;), γνωρίζετε ότι όταν αναλύονται τα αρχεία .js διαβάζονται από πάνω προς τα κάτω. Ενώ, ναι, η ανύψωση μπορεί να θολώσει λίγο τις γραμμές στη σειρά, είναι η καλύτερη πρακτική να οργανώνετε τον κώδικά σας με αυτόν τον διαδοχικό τρόπο και κάνει επίσης τον εντοπισμό σφαλμάτων πολύ πιο εύκολο.

Ας δούμε λοιπόν την εφαρμογή Express (η οποία ζει στο app.js) από πάνω προς τα κάτω:

Οι εισαγωγές πρέπει πάντα να δηλώνονται στο επάνω μέρος της αίτησής σας. Για το backend μου Express, αυτό περιλαμβάνει εισαγωγές πακέτων, εισαγωγές δρομολογίων και εισαγωγές μοντέλων.

Χρειάζομαι μόνο δύο εισαγωγές πακέτων σε αυτό το αρχείο app.js (άλλα χρησιμοποιούνται σε αρχεία βάσης δεδομένων, μοντέλου ή ελεγκτή):

const express = require(“express”);
const bodyParser = require(“body-parser”);

Στη συνέχεια, εισαγάγετε το αρχείο των διαδρομών μου:

const routes = require(“./routes/routes”)

Και τέλος το παράδειγμα της βάσης δεδομένων μου με τα μοντέλα που θα χρησιμοποιήσω:

const db = require(“./models/index”)
const Song= require(“./models/song”)
const Mood = require(“./models/mood”)
const Playlist = require(“./models/playlist”)
const User = require(“./models/user”)
const PlaylistSong = require(“./models/playlist-song”)

(Προφανώς για διαδρομές και μοντέλα, οι διαδρομές των αρχείων σας μπορεί να είναι διαφορετικές, καθώς αναφέρονται σε τοπικά αρχεία.)

Εντάξει ρε! Οι εισαγωγές γίνονται. Σχεδόν θα εκπλαγείτε πόσο εύκολο είναι να ξεχάσετε κάποια εισαγωγή, επειδή είστε ενθουσιασμένοι που μόλις ξεκινήσατε και στη συνέχεια να φωνάξετε την εφαρμογή σας επειδή δεν ξέρει πού να βρει κάτι. Θα έλεγα ότι τουλάχιστον τα μισά πιθανά σφάλματα μπορούν να αποφευχθούν αν σκεφτεί κανείς πρώτα τον προγραμματισμό — "τι εξαρτήσεις έχει αυτό το αρχείο;"

Ακολουθεί η πολύ σύντομη αλλά αναμφισβήτητα πιο σημαντική δήλωση για αυτό το αρχείο:

const app = express();

Αυτό δημιουργεί ένα στιγμιότυπο του Express που θα εξοπλιστεί με μια ισχυρή διεπαφή για την εφαρμογή μας, πράγματα όπως οι μέθοδοι use() και listen() ενσωματώνονται σε αυτήν την περίπτωση Express.

Το επόμενο είναι σημαντικό boilerplate εάν σκοπεύετε να έχετε διαφορετικούς τομείς front-end και back-end - κάτι που για τα περισσότερα API θα ισχύει και σε κάθε περίπτωση είναι μια καλή ευελιξία να έχετε. Λόγω των ελέγχων ασφαλείας στο πρόγραμμα περιήγησης, δεν επιτρέπεται σιωπηρά να μοιράζεστε πόρους (όπως δεδομένα API) μεταξύ δύο διαφορετικών τομέων (διαβάστε περισσότερα στο "CORS"), επομένως πρέπει να ενεργοποιήσουμε αυτήν την κοινή χρήση μεταξύ προέλευσης ρητά στο app.js μας ροή:

app.use( (req, res, next) => {
 res.setHeader(“Access-Control-Allow-Origin”: “*”);
 res.setHeader(“Access-Control-Allow-Methods”: “*”);
 res.setHeader(“Access-Control-Allow-Headers”: “Content-Type, Authorization”);

Αυτή είναι μια πολύ ανοιχτή (διαβάστε: όχι πολύ ασφαλής) ενεργοποίηση CORS, αλλά δεν φτιάχνω μια εφαρμογή για τη CIA, φτιάχνω μια μικρή εφαρμογή για δεδομένα χωρίς άδεια χρήσης, οπότε προς το παρόν, σε εξέλιξη, θέλω να χρησιμοποιήσω μια αρκετά ευγενική πλάκα λέβητα.

Σημειώστε επίσης ότι το app.use() για όλες τις διαδρομές θα λάβει μια παρόμοια συνάρτηση, με ορίσματα για αίτημα, απάντηση και επόμενο. Το όρισμα req (αίτημα) θα περιέχει όλα τα δεδομένα που έρχονται με το αίτημα, συμπεριλαμβανομένων των κεφαλίδων, του σώματος, των παραμέτρων, κ.λπ. προσθέστε αυτές τις κεφαλίδες έτσι ώστε όταν το στέλνουμε στο πρόγραμμα περιήγησης, το πρόγραμμα περιήγησης να μπορεί να δει ότι έχουμε επιτρέψει την κοινή χρήση μεταξύ προέλευσης. Η συνάρτηση next() απλώς περνά στο επόμενο ενδιάμεσο λογισμικό.

Ω, σωστά, ενδιάμεσο λογισμικό - τι είναι αυτό; Βασικά ό,τι υπάρχει μεταξύ της παρουσίασης Express και της δήλωσης διακομιστή (app.listen()) είναι ένα ενδιάμεσο λογισμικό, τελικά ζει στη μέση. Αυτό το ενδιάμεσο λογισμικό μεταλλάσσει την απόκριση όπως είναι απαραίτητο μεταξύ της παραλαβής αιτήματος και της αποστολής απόκρισης και περιλαμβάνει όλη τη δρομολόγηση και την πρόσβαση στα δεδομένα που θα κάνουμε. Η εφαρμογή Express μας είναι βασικά μια μεγάλη οικογένεια ενδιάμεσων λογισμικών με κάποιο περιθώριο στα δύο άκρα.

Στη συνέχεια, θα δρομολογήσουμε το αίτημα στη σωστή διαδρομή ή τελικό σημείο. Επέλεξα να αποθηκεύσω τις διαδρομές μου στο δικό τους αρχείο, οπότε το μόνο που θα βρείτε στο app.js μου είναι:

app.use(routes);

Αυτό αναφέρεται στο αρχείο διαδρομών μου, όπου ορίζω τις διαδρομές μου ως εξής:

const router = express.Router();
router.get(“/get-home-moods”, controllers.getHome);
router.get(“/moods/:moodId”, controllers.getMood);
router.post(“/add-music”, controllers.postSong);
router.post(“/new-user”, controllers.postUser);
router.post(“/login”, controllers.login);
router.post(“/love-song/:songId”, controllers.postAddSongToPlaylist);
router.get(“/playlists/:playlistId”, controllers.getPlaylist);
router.post(“/playlists/remove-song/:songId”, controllers.postRemoveSongFromPlaylist);

Αυτές οι διαδρομές ορίζουν τα τελικά σημεία μας και κατευθύνουν το αίτημα στο σωστό σύνολο λογικής για την επίλυση του αιτήματος. Το Express και πάλι μας εξυπηρετεί καλά με την ήδη ενσωματωμένη μέθοδο ρύθμισης του Router() για εμάς. Ευχαριστώ, Express!

Τελευταία στο αρχείο μας app.js, είναι η ρύθμιση της λογικής για τη βάση δεδομένων μας και ο καθορισμός του τρόπου αλληλεπίδρασης των μοντέλων. Για να παραμείνω αγνωστικιστής της βάσης δεδομένων, θα το παραλείψω και θα προχωρήσω στη ρύθμιση του διακομιστή μας για να ακούει αιτήματα:

db.sync()
 .then(() => {
 app.listen(process.env.PORT || 3000);
 });

Αυτό θα διασφαλίσει ότι η βάση δεδομένων μας συγχρονίζεται και, εάν είναι επιτυχής, θα εκκινήσει τον διακομιστή μας! Κάνω έρωτα! Επειδή η εφαρμογή μου έχει αναπτυχθεί, πρέπει να πάρει τον αριθμό θύρας από το Heroku, γι' αυτό χρησιμοποιώ το process.env.PORT που αποθηκεύει τις μεταβλητές περιβάλλοντος ανάπτυξης, αλλά για ανάπτυξη μπορείτε να χρησιμοποιήσετε 3000 ή 8080 κ.λπ.

Γράψτε τους ελεγκτές σας

Δεν θα περάσω από όλους τους ελεγκτές μου, αλλά θα καλύψω δύο βασικά είδη, επειδή η μικρή διαφορά μεταξύ τους με οδήγησε σε ένα μακρύ ταξίδι αντιμετώπισης προβλημάτων και αναζήτησης του σημείου που εμφανίστηκε το σφάλμα, αργά στην εφαρμογή μου. Και θέλω να σας γλυτώσω από τον πονοκέφαλο!

Ας εξετάσουμε πρώτα τη διαδρομή "/login" και τον ελεγκτή της. Ένας χρήστης θα πληκτρολογήσει ένα όνομα χρήστη και έναν κωδικό πρόσβασης και, στη συνέχεια, το backend μας θα ελέγξει αν υπάρχει χρήστης με αυτό το όνομα χρήστη και εάν ναι, εάν ο κωδικός πρόσβασης που πληκτρολογήθηκε ταιριάζει με αυτόν που υπάρχει στο αρχείο. Εάν αυτό είναι επιτυχές, θα στείλουμε πίσω ένα διακριτικό που θα αποθηκευτεί στη διεπαφή για μελλοντικά αιτήματα και θα τους παραχωρήσουμε πρόσβαση σε προστατευμένους πόρους (στο frontend, θα χρησιμοποιήσουμε αυτό το διακριτικό για να τους δρομολογήσουμε αυτόματα στην αρχική σελίδα).

Ας δούμε τον κώδικα:

login = (req, res, next) => {
 const user = req.body.username;
 const password = req.body.password;
 User.findByPk(user)
 .then(user => {
  return bcrypt.compare(password, user.password);
 })
 .then(isEqual => {
  if (!isEqual){
   throw new Error(“Incorrect Password”);
  }
  const token = jwt.sign({email: user}, “secretkey”, {expiresIn: “1hr”})
  return res.status(200).json({token: token, user: user});
 )}
};

Εντάξει, ξέρω, έχει πολλά να της κάνει μόνο και μόνο για να συνδεθεί κάποιος.

Πρώτα εξάγουμε το όνομα χρήστη και τον κωδικό πρόσβασης από το αίτημα — στο μπροστινό μέρος αυτά εισάγονται από τον χρήστη σε μια απλή φόρμα και υποβάλλονται με ένα κουμπί υποβολής.

Στη συνέχεια, έχουμε πρόσβαση στη βάση δεδομένων μας μέσω του μοντέλου Χρήστη (κεφαλαίο-U), για να βρούμε έναν συγκεκριμένο χρήστη με βάση το πρωτεύον κλειδί του, που σε αυτήν την περίπτωση είναι το όνομα χρήστη του.

Μόλις το κάνουμε αυτό (που είναι παρεμπιπτόντως μια ασύγχρονη συνάρτηση και επιστρέφει μια υπόσχεση, γι' αυτό χρησιμοποιούμε ένα μπλοκ then()), πρέπει να πάρουμε αυτόν τον κωδικό πρόσβασης και να τον συγκρίνουμε με τον κρυπτογραφημένο που έχουμε που είναι αποθηκευμένα στη βάση δεδομένων μας, χρησιμοποιούμε το bcrypt για αυτό και επιστρέφει επίσης μια υπόσχεση.

Στη συνέχεια, επαληθεύουμε ότι αυτοί οι κωδικοί πρόσβασης ταιριάζουν πράγματι. Εάν δεν το κάνουν, θέλουμε να κάνουμε ένα σφάλμα για να το υποδείξουμε. Εάν ταιριάζουν τότε δημιουργούμε ένα διακριτικό (εδώ χρησιμοποιώ το πακέτο jsonwebtoken. Και μετά στέλνουμε αυτό το διακριτικό πίσω στο frontend με τις πληροφορίες χρήστη σε ένα JSON, χρησιμοποιώντας το .json({ }) μέθοδος.

Αρκετά απλό, σωστά; Ας το συγκρίνουμε με το τελικό σημείο και τον ελεγκτή "/new-user", για να δούμε πού κρυβόταν η Μικρή απόχρωση που μου προκάλεσε μεγάλο πονοκέφαλο:

newUser = (req, res, next) => {
 const user = req.body.username;
 const password = req.body.password;
User.findByPk(user)
 .then(user => {
  if (user) {
   res.status(303);
   throw new Error("Username is already in use");
  }
  return bcrypt.hash(password, 12);
 })
 .then(hashedPass => {
  return User.create({username: user, password: hashedPass});
 })
 .then(()=> {
  return res.status(200).end();
 })
};

Εντάξει, λοιπόν, λαμβάνουμε τα ίδια δεδομένα από τη διεπαφή μας όπως πριν — ένα όνομα χρήστη και έναν κωδικό πρόσβασης, εύκολα.

Στη συνέχεια, έχουμε ξανά πρόσβαση στη βάση δεδομένων μας μέσω του μοντέλου δεδομένων μας, αλλά αυτή τη φορά πρέπει να βεβαιωθούμε ότι δεν υπάρχει ήδη χρήστης με αυτό το όνομα χρήστη. Εάν υπάρχει, ρίχνουμε ένα Σφάλμα, αλλά αν δεν υπάρχει κρυπτογραφούμε αυτόν τον κωδικό πρόσβασης.

Στη συνέχεια δημιουργούμε έναν νέο χρήστη στη βάση δεδομένων μας με το σωστό όνομα χρήστη και κωδικό πρόσβασης. Μέχρι στιγμής όλα καλά (στην αίτησή μου, εισάγω επίσης ένα άλλο μπλοκ then() για να δημιουργήσω αυτόματα μια λίστα αναπαραγωγής "Τα κομμάτια που μου αρέσουν", αλλά το έχω αποκλείσει εδώ για να εστιάσω στο…

Τέλος, στέλνουμε πίσω την απάντηση, η οποία είναι ένας κωδικός κατάστασης 200 για να σηματοδοτήσει ότι δημιουργήσαμε με επιτυχία τον χρήστη. Στο frontend, η λήψη ενός κωδικού κατάστασης 200 ενεργοποιεί τη λειτουργία σύνδεσης και μια αυτόματη αναδρομολόγηση στην αρχική μας σελίδα. Αλλά για το μεγαλύτερο χρονικό διάστημα δεν μπορούσα να το κάνω να λειτουργήσει και σκέφτηκα ότι ίσως ήταν κάποιο λάθος στη λογική μου για τη μέθοδο history.push του React Router.

Δεν μπορούσα να καταλάβω γιατί η διαδρομή σύνδεσής μου ανακατευθυνόταν, αλλά η διαδρομή εγγραφής μου όχι! Κάθε νέος χρήστης θα εξακολουθούσε να προστίθεται στη βάση δεδομένων μου. Κάθε scaffolded console.log() τόσο στο μπροστινό όσο και στο πίσω μέρος θα ενεργοποιούνταν, αλλά για κάποιο λόγο τα αιτήματά μου θα πέθαιναν κάπου πριν ολοκληρωθούν και μετά από λίγα λεπτά αναμονής, θα λαμβανόταν ένας κωδικός αποτυχίας 503…

Αλλά η πηγή του προβλήματός μου ήταν ότι είχα αρχικά σε αυτό το τελικό τότε μπλοκ κώδικα:

return res.status(200);

Επειδή οι περισσότερες από τις σημαντικές διαδρομές μου έστειλαν δεδομένα JSON πίσω στο μπροστινό μέρος, μπόρεσα να αποφύγω αυτό το σφάλμα για τις περισσότερες διαδρομές. Και η ρίζα αυτού του σφάλματος είναι ότι η μέθοδος απόκρισης .json() έχει μια μέθοδο ενθυλακωμένη(read: invisible) end() σε αυτήν, η οποία προκαλεί την αποστολή της απάντησης. Η μέθοδος status() δεν προορίζεται να τερματίσει και να στείλει απαντήσεις όχι, και έτσι πρέπει να κληθεί μια ρητή μέθοδος end() για να σταλεί η απάντηση με επιτυχία!

Εκ των υστέρων, αυτό φαίνεται σαν μια ανόητη παράβλεψη, αλλά δείχνει το διπλό αποτέλεσμα της ενθυλάκωσης της λογικής. Ενώ οι ενθυλακωμένες διεπαφές διαφόρων πακέτων, συμπεριλαμβανομένου του Express, κάνουν το έργο ενός προγραμματιστή πολύ πιο εύκολο, μπορεί επίσης να σας κάνει να ξεχάσετε μερικά από τα βασικά και να δυσκολευτείτε να βρείτε την απάντηση όταν κάτι χαλάει αόρατα.

Λοιπόν, τώρα έχουμε τακτοποιήσει το backend μας — τι γίνεται με το frontend;