Express + TypeScript - konfiguracja projektu

Wstęp

Opisuje tutaj prosty sposób na rozpoczęcie pracy z TypeScripty i Node a dokładniej z jego frameworkiem Express. W tym wpisie powstanie całkiem prosty boilerplate aplikacji, która w kolejnych artykułach rozwijana będzie o coraz to bardziej zaawansowane elementy. Omówię konfiguracje projektu, strukturę aplikacji, integracje z bazami danych o nie tylko.

Ten wpis, jak i kolejne, zawiera kompletny omawiany kod w repozytorium github.

Właściwie po co?

Express.JS

Express jest to jeden z bardziej popularnych frameworków Node, służący do pisania aplikacji backendowych. Jest prosty, minimalistyczny i bardzo niewiele narzuca programiście. Struktura aplikacji może być praktycznie dowolna, a framework nie zmusza do stosowania żadnego patternu, internet natomiast pełen jest dobrych praktyk i wzorców pisania aplikacji.

Express świetnie sprawdza się do budowania API typu REST i aplikacji CRUD, oraz adaptowania w aplikacjach różnego rodzaju baz danych. Prosta aplikacja pisana w tym i kolejnych artykułach będzie wykorzystywać prostotę i wielofunkcyjność framewoku oraz bogactwo różnych możliwości rozszerzania aplikacji w nim pisanych.

Zazwyczaj, gdy pojawia się hasło Express.JS, od razu nasuwa się na myśl Rest'owe API napisane w JaVaScript. Będąc świadomym ograniczeń JS wynikających z braku typowania, ale nadal chcąc użyć prostego, dobrze sprawdzającego się i pozwalającego na dowolną rozbudowę aplikacji frameworka, jakim jest Express? Postanowiłem napisać aplikację przy użyciu TypeScripta? Jak się niedługo okaże, framework i język współpracują ze sobą świetnie!

Początek

Pierwszym krokiem powinno być sprawdzenie, że posiadam zainstalowane na komputerze Node i NPM.

terminal
node -v
npm -v

Później standardowo przenoszę się do miejsca, w którym chcę rozpocząć projekt. W wybranym katalogu, jak w przypadku każdego projektu w Node, rozpoczynam od inicjalizacji projektu. Flaga -y pomija wszystkie pytania, które zadaje nam CLI i uzupełnia je defaultami.

terminal
npm init -y

Obok, w ten sposób stworzonego pliku package.json, tym samym katalogu zakładam folder /src a w nim plik index.js, który będzie wyjściowym plikiem aplikacji.

W pliku package.json zapisywane są wszystkie paczki, z których korzystam w projekcie. Poza dependencjami, zapisane tu są również podstawowe dane o projekcie. Skupiam się tu jedynie na przygotowaniu startera do późniejszego wykorzystania w innych aplikacjach. Na początek trzeba tylko zmienić pole "main" z index.js na src/index.js zgodnie z drzewem plików, który utworzyłem wyżej.

package.json
{
  "name": "express-ts",
  "version": "1.0.0",
  "description": "",
  "main": "src/index.js",
  "type": "module",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

Teraz, aby pokazać, że aplikacja w ogóle działa, nie skupiając się jeszcze na tym, co oferuje TypeScript i Express, wypełniam plik src/index.js najprostszym możliwym "serwerem", którego jedynym zadaniem jest nasłuchiwanie na localhost:5000/ i zwracanie "Hello World!". Warto zauważyć, że pierwsza linijka importuje express w nowy sposób, wskazujący na importowanie zależności na podstawie modułów. Jest to możliwe przez podanie w package.json typu aplikacji jako "type": "module".

src/app.js
import express from "express";

const app = express();

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

app.listen(5000, () => {
  console.log("Server started on port 5000!");
});

Aby być w stanie odpalić serwis, muszę zainstalować jego dependencje, czyli sam Express framework.

terminal
npm i express

Po instalacji aplikację można wystartować w terminalu, z poziomu katalogu projektu:

terminal
node src/index.js

W terminalu pojawiła się informacja, że aplikacja wystartowała na porcie 5000, a po wywołaniu w przeglądarce adresu http://localhost:5000 ukaże się tylko "Hello world!".

Inicjalizacja TypeScript

Aby móc używać w aplikacji TypeScript i dobrodziejstw z niego płynących, zainstaluję go jak dev-dependency wraz bardzo przydatnymi deklaracjami typów dla Node i Express. Deklaracje typów to paczki, które można zainstalować poprzez NPM, zawierające nazwę paczki, dla której typów potrzebuję, poprzedzone @types/..., predefiniujące typy i kształty obiektów, wykorzystywanych przez daną paczkę. W tym wypadku będzie to @types/express i ogólny @types/node.

terminal
npm i -D typescript @types/express @types/node

Kolejnym krokiem jest inicjalizacja TypeScrypt w projekcie.

terminal
npx tsc --init

W ten sposób generuję plik tsconfig.json, w którym zapisane są wszystkie parametry, mówiące o tym w jaki sposób TS ma budować projekt (jako budowanie rozumiem generowanie - kompilacje, plików JS, które później będą wykorzystywane do działania aplikacji). Na sam początek dostajem bardzo duży plik z wieloma wykomentowanymi parametrami, można spokojnie usunąć wszystko, co wykomentowane.

Najważniejsze parametry tsconfig.json:

  • target: do jakiej wersji JS kompilowane będą pliki
  • module: w jaki sposób ma zachowywać się rozwiązywanie modułów w skompilowanym kodzie
  • strict: czy używać strict type-checking

Pozostałe defaulty mogą pozostać bez rozwinięcia.

Dodatkowo parametrami, które warto przekazać w tsconfig.jsonrootDir i outDir, które mówią odpowiednio o tym, z jakiego katalogu ma być zaciągany kod .ts i do jakiego ma być zapisywany generowany kod .js.

tsconfig.json
{
  "compilerOptions": {
    "rootDir": "./src",
    "outDir": "./dist",
    "target": "es2020",
    "module": "commonjs",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true
  }
}

Po tak dodanym kodzie TypeScrypt do projektu można zmienić sam plik src/index.js na src/index.ts. Również jego zawartość nieznacznie się zmieni, aby wykorzystać zainstalowane wcześniej deklaracje typów.

src/index.ts
import express, { Request, Response } from "express";

const app = express();

app.get("/", (req: Request, res: Response) => {
  res.send("Hello World!");
});

app.listen(5000, () => {
  console.log("Server started on port 5000!");
});

Tak naprawdę wcale nie trzeba by było zmieniać zawartości pliku src/index.ts. Czysty kod JS jest w 100% rozumiany przez TS, więc aplikacja działałaby tak samo.

Tak przebudowany projekt pisany w TS możena teraz zkompilować do plików JS jedną komendą terminala: npx tsc.

Pliki .js generują się w katalogu /dist, więc aby odpalić aplikację, będę wywoływać plik właśnie tam się znajdujący.

terminal
node dist/index.js

Skrypty w package.json

Aby usprawnić pracę kodem, defininiuję skrypty w package.json odpalające pod spodem zdefiniowane wcześniej komendy budowania i odpalania aplikacji.

package.json
{
  "name": "express-ts",
  "version": "1.0.0",
  "main": "src/index.js",
  "scripts": {
    "build": "npx tsc",
    "start": "node dist/index.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "dependencies": {
    "express": "^4.18.2"
  },
  "devDependencies": {
    "@types/express": "^4.17.14",
    "@types/node": "^18.11.13",
    "typescript": "^4.9.4"
  }
}

rebuild & restart on file change

Ostatnią godną uwagi rzeczą, jest automatyczny rebuild i restart aplikacji, gdy pliki .ts się zmieniły. Pomogą w tym dwa pakiety, które zainstaluję jak devDepenencje.

terminal
npm i -D nodemon ts-node

Można teraz dodać w package.json kolejny skrypt, "start:dev": "nodemon src/index.ts", który po odpaleniu będzie obserwował wszystkie pliki .ts, gdy któryś się zmieni, przebuduje aplikację i ponownie ją odpali, już ze zmienionym kodem.

package.json
{
  "name": "express-ts",
  "version": "1.0.0",
  "main": "src/index.js",
  "scripts": {
    "build": "npx tsc",
    "start": "node dist/index.js",
    "start:dev": "nodemon src/index.ts"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "dependencies": {
    "express": "^4.18.2"
  },
  "devDependencies": {
    "@types/express": "^4.17.14",
    "@types/node": "^18.11.13",
    "nodemon": "^2.0.20",
    "ts-node": "^10.9.1",
    "typescript": "^4.9.4"
  }
}

Podsumowanie

Tak stworzony boilerplate może posłużyć do tworzenia kolejnych, bardziej zaawansowanych projektów przy użyciu frameworku Express, a dzięki wsparciu TypeScript kod będzie zawsze czytelny, wolny od oczywistych błędów związanych z typowaniem i optymalnie skompilowany pod produkcję. Cały kod powyższej aplikacji można znaleźć w repozytorium github.