Proste RESTowe API (Node.js + express.js + TypeScript + MongoDB) cz.1

Moi drodzy, ruszamy na back-end! Dzisiaj będziemy pisać bardzo proste RESTowe API, które pomoże każdemu frontowi stworzyć prosty back-end do swojego projektu.

Historia zaczyna się od tego, że w newsletterze dostałam linka o artykule o prostym set upie node.js + express.js + Typescript. Gdzieś ostatnio myślałam o przepisaniu portfolio na jakiś reaktywny framework JS, a do tego przydałoby się API. Akurat miałam wolny sobotni wieczór, a Youtube podpowiedział mi hit Pawła Domagały.

Czy muszę mówić coś więcej? Zapraszam :)

Słowem wstępu

Jak zwykle zaczynam od sekcji dla początkujących, ale trudno mi wyjaśnić wszystkie aspekty opisane w niniejszym poście. Zdecydowanie sekcja dla początkujących jest skierowana dla osób, które miały do czynienia z podstawami programowania obiektowego.

NodeJS to środowisko uruchomieniowe, które pozwala na interpretację JavaScriptu, stworzone pod pisanie serwerów i aplikacji internetowych.

Express.js to jedna z popularniejszych bibliotek do budowania serwisów opartych o protokół HTTP. Obsługuje wiele popularnych silników renderowania, ale dzisiaj nie będziemy z nich korzystać.

TypeScript to silnie typowany język programowania, który przetwarza się na JavaScript (tzw. nadzbiór języka). Pozwala na programowanie obiektowe oparte na klasach oraz typach :) Osobiście podsumowałabym to jako całkiem udane dziecko C# i JavaScriptu, rozwijane pod skrzydłami Microsoftu.

REST - albo w sumie RESTowe (ang. RESTful) serwisy - to bardzo popularna koncepcja projektowania web serwisów.

NPM - manager do zarządzania pakietami JavaScript.

TSLint - wygodne sprawdzanie kodu napisanego w TypeScript, osobiście używam go w edytorze VS Code.

Rozpoczynamy!

Utwórzmy sobie folder, wejdźmy do niego w konsoli i szalejmy:

npm init

Tutaj swobodnie wpiszcie sobie dane projektu przez konfigurator; nie przejmujcie się, to można edytować w pliku.

npm install -D typescript
npm install -D tslint
npm install -D nodemon
npm install express
npm install @types/express
npm install mongoose
npm install @types/mongoose

Powyższe komendy trzeba puścić w konsoli. Ogólnie @types to są definicje typów bibliotek w TypeScript, mongoose to biblioteka do operacji bazowo-danowych w MongoDB.

Nodemon służy do przyjemnego programowania w nodeJS, nasłuchuje i przeładowuje środowisko po zmianach. TSLint podpowie nam składnię czy też strukturę obiektów.

Konfiguracja

Poniższą komendą tworzymy plik konfiguracyjny TSLint. Najlepiej pobrać tslint:recommended i nadpisać to co się nam nie podoba w rules.

./node_modules/.bin/tslint --init

Tak wygląda mój po drobnych zmianach:

{
    "defaultSeverity": "error",
    "extends": [
        "tslint:recommended"
    ],
    "jsRules": {},
    "rules": {
        "interface-name": [false],
        "no-console": [false],
        "quotemark": [true, "single"],
        "object-literal-sort-keys": false
    },
    "rulesDirectory": []
}

Teraz edytujemy plik package.json i dołożymy tam skróty skryptów, żeby łatwiej było nam uruchamiać aplikację :)

Tak wygląda mój plik package.json:

{
    "name": "api",
    "version": "1.0.0",
    "description": "Przykładowy opis",
    "main": "dist/server.js",
    "scripts": {
        "build-ts": "tsc",
        "start": "npm run serve",
        "serve": "node dist/server.js",
        "watch-node": "nodemon dist/server.js",
        "watch-ts": "tsc -w"
    },
    "author": "",
    "license": "ISC",
    "devDependencies": {
        "nodemon": "^1.18.4",
        "tslint": "^5.11.0",
        "typescript": "^3.0.3"
    },
    "dependencies": {
        "@types/express": "^4.16.0",
        "@types/mongoose": "^5.2.17",
        "express": "^4.16.3",
        "mongoose": "^5.2.17"
    }
}

Nasza baza - MongoDB lokalnie

Ze strony MongoDB należy pobrać instalator lub paczkę i zainstalować, a potem uruchomić plik exe. Osobiście korzystam z konsoli Bashowej :)

Windowsowo będzie to wyglądało na przykład:

cd twojasciezka/MongoDB/Server/3.4/bin
./mongod.exe

To dwa polecenia. Napisane są osobno, żebyście mogli wykryć błędną ścieżkę.

Jeśli twoją konsolę zalał multum informacji i nie pokazał się żaden error, osiągnęliśmy sukces i przechodzimy do programowania.

Zadanie

Chcę, żeby adres /projects zwrócił mi listę projektów w formacie JSON.

To do roboty!

Najpierw uruchomimy aplikację z biblioteką express.js. Tworzymy plik server.ts w folderze src:

/src/server.ts

import express from 'express';
const app = express();

app.get('/', (req, res) => res.send('Hello World!'));

app.listen(3000, () => console.log('Example app listening on port 3000!'));

W tej chwili po wywołaniu:

npm run build-ts
npm run start

W konsoli powinien się nam pojawić komunikat: `Example app listening on port 3000!`, a na http://localhost:3000 powinien być tekst 'Hello Word!'

Teraz włączymy na słuchiwanie, żebyśmy nie musieli powyższych komend wywoływać po każdej zmianie. Do tego musimy otworzyć 2 konsole:

npm run watch-node
npm run watch-ts

API

Całość będzie wykonana w popularnym wzorcu architektonicznym MVC, w dość uproszczonej wersji. W ogóle będzie ekstremalnie prosto: Nie będzie repo, interfejsy ograniczymy do minimum (czasem nawet niebezpiecznego, o czym opowiem) i czasem wrzucimy parę rzeczy do jednego pliku. Nie będzie także żadnej autoryzacji, co jest ogólnie zła praktyką.

W poniższym pliku server.ts mamy znane z początku wywołanie aplikacji oraz parę nowości :)

/src/server.ts
import express from 'express';
import mongoose from 'mongoose';
const app = express();

import * as projectController from './controllers/project';

mongoose.connect('mongodb://localhost:27017/').then(
    () => {
        console.log('Udało się!');
    },
  ).catch((err) => {
    console.log('MongoDB connection error. Please make sure MongoDB is running. ' + err);
    // process.exit();
  });

app.get('/', (req, res) => res.send('Hello World!'));
app.get('/projects', projectController.getProjects);

app.listen(3000, () => console.log('Example app listening on port 3000!'));

Przede wszystkim metoda mongoose.connect w której łączymy się z bazą danych. Łączenie się z lokalną bazą nie wymaga podania nazwy użytkownika oraz hasła, a port 27017 jest domyślny dla MongoDB. Wszystkie ustawienia mogliście zmienić podczas instalacji. Czy muszę wspominać, że trzymanie danych do bazy w pliku server.ts to zły pomysł i jest to tylko na potrzeby tutorialu? Myślę, że nie.

Ale wracając do kodu:  mongoose.connect zwraca obietnicę (ang. Promise) i jeśli będzie ona sukcesem to mamy wesoły komunikat :) Jeśli nie to odpowiedź zwróci wyjątek, który złapiemy catch'em i mamy smutna informację z wyjaśnieniem. Można też - jak w zakomentowanym fragmencie - wyłączyć aplikację poleceniem process.exit().

Kolejną nowością jest przypisanie ścieżek do funkcji, czyli zdefiniowanie routingów.

http://localhost:3000 zwróci nam Hello World!

http://localhost:3000/projects zwróci nam to co zwraca funkcja, którą przypisaliśmy.

Teraz przechodzimy do import z kontrolerem, gdzie ładujemy cały kontroler i przypisujemy poszczególną funkcję do routingu.

A teraz kontroler, który zwróci nam dane w formacie JSON.

src/controllers/project
import { Request, Response } from 'express';
import * as mongoose from 'mongoose';
import { ProjectModel } from '../schemas/projectSchema';

export let getProjects = (req: Request, res: Response) => {
    ProjectModel.find({}, (err, project) => {
        if (err) {
            res.send(err);
        }
        res.json(project);
    });
};

Funkcje, które przypisujemy do routingów zawierają dwa typy zmiennych: request czyli dane z zapytania oraz response, czyli to co wyślemy. Ponieważ wiem, że uwielbiacie metafory to pomyślcie o funkcji w kontrolerze jak o stołówce: prosisz o mielonego z ziemniakami (request) i dostajesz mielonego z ziemniakami (response). Ty jesteś np.: przeglądarką internetową, a serwer lub NodeJS obsługą. Proste, prawda? Chyba, że stołówka jest zamknięta, obsługa się pomyli albo nie ma już ziemniaków.

Funkcja `res.json(dane)` zwróci 200, a w nagłówku typem danych będzie JSON.

ProjectModel to obiekt udostępniany przez mongoose, którego interfejs pozwala przeprowadzać CRUDowe (Create, Read, Update, Delete) operacje na dokumencie w bazie danych.

Na potrzeby wpisu nasze projekty będą miały tylko jedną właściwość - name. ProjectModel nie jest jednak domyślny ani automatycznie generowany, co stwarza trochę kłopotów - o czym dalej :)

Dlatego stworzymy sobie taki prosty interfejs bez którego można się obejść, jeśli aplikacja nie będzie rozwijana. Nasza na szczęście będzie :)

//src/interfaces/IProject
export interface IProject {
    name?: string;
}

I tutaj powinniśmy napisać jeszcze interfejs do schemy w TypeScripcie, który dziedziczy po Document. Trzeba by te dwa interfejsy trzymać w miarę spójne, do czego zostały stworzone osobne biblioteki nawet.

My zastosujemy tanią sztuczkę, która co prawda nas ogranicza, ale także zmniejsza ilość pracy :)

type ProjectDocument = IProject & Document;

Definicja nowego typu poleceniem Type pozwala nam stworzyć typ, który ma właściwości naszego interfejsu (w tym przypadku name) oraz metody z mongoose dla dokumentu. Dokładnie to czego w tej chwili potrzebujemy, chociaż nie jest to dobra praktyka w długofalowym projekcie.

import { Schema, Document, Model, model } from 'mongoose';
import { IProject } from '../interfaces/IProject';

type ProjectDocument = IProject & Document;

const ProjectSchema = new Schema({
    name: {
        type: String
    }
});

export const ProjectModel: Model<ProjectDocument> = model<ProjectDocument>('ProjectModel', ProjectSchema);

Poniżej definiujemy schemę spójną z naszym interfejsem, a finalnie exportujemy ProjectModel, który jest modelem z zaimplementowanym interfejsem ProjectDocument.

A teraz czekamy, aż wszystko się przeładuje i... wchodzimy na:

http://localhost:3000/projects i patrzymy na pusty obiekt JSON, który jest dzisiejszym celem!

Osiągnęliśmy R z CRUD i w następnych częściach już śmiało pójdziemy dalej :)

Podsumowanie

To był niezwykle satysfakcjonujący wpis, szaleję za NodeJSem. Ponieważ nie mogłam wszystkiego wytłumaczyć, śmiało zadawajcie pytania i wytykajcie błędy :) Na githuba przyjdzie pora wraz z drugą częścią.

A jak nie wiecie co napisać, to napiszcie chociaż jak angażować użytkowników w komentowanie :) Poza popełnianiem błędów oczywiście.

Kamila

Dziękuję za poświęcony czas, będzie mi bardzo miło jak zostawisz komentarz :)