TypeScript et Node.js#
Node.js et TypeScript forment aujourd’hui l’une des combinaisons les plus populaires pour le développement côté serveur. Node.js apporte un environnement d’exécution JavaScript rapide, asynchrone et doté d’un écosystème npm gigantesque ; TypeScript y ajoute la sûreté statique des types, l’autocomplétion et la détection d’erreurs à la compilation. Ensemble, ils permettent de construire des serveurs HTTP, des CLI, des scripts d’automatisation et des microservices avec une productivité et une fiabilité remarquables. Ce chapitre guide l’installation initiale jusqu’au déploiement en production, en couvrant les spécificités de l’environnement Node.js.
Mise en place d’un projet Node.js + TypeScript#
Structure recommandée#
mon-projet/
├── src/ ← code source TypeScript
│ ├── index.ts ← point d'entrée
│ ├── routes/ ← définitions de routes
│ │ └── utilisateurs.ts
│ ├── services/ ← logique métier
│ │ └── utilisateur.service.ts
│ └── types/ ← déclarations de types locales
│ └── globaux.d.ts
├── dist/ ← sortie compilée (généré par tsc)
├── node_modules/ ← dépendances npm
├── package.json
├── package-lock.json
└── tsconfig.json
package.json de départ#
{
"name": "mon-projet",
"version": "1.0.0",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsx watch src/index.ts",
"lint": "eslint src --ext .ts",
"test": "jest"
},
"devDependencies": {
"typescript": "^5.4.0",
"@types/node": "^20.0.0",
"tsx": "^4.0.0"
}
}
tsconfig.json pour Node.js#
La configuration diffère selon que le projet utilise CommonJS (le mode historique de Node.js) ou ESM natif (recommandé pour les nouveaux projets Node.js 18+).
Pour CommonJS :
{
"compilerOptions": {
"target": "ES2022",
"module": "CommonJS",
"moduleResolution": "node",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Pour ESM natif (Node.js ≥ 16, "type": "module" dans package.json) :
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}
Outils d’exécution directe#
Pour le développement, recompiler avec tsc à chaque modification est fastidieux. Plusieurs outils permettent d’exécuter directement des fichiers TypeScript :
# tsx — le plus rapide, basé sur esbuild
npm install --save-dev tsx
npx tsx src/index.ts
npx tsx watch src/index.ts # mode surveillance
# ts-node — le plus ancien, supporte davantage de cas particuliers
npm install --save-dev ts-node
npx ts-node src/index.ts
# esbuild — pour compiler très rapidement vers dist/
npm install --save-dev esbuild
npx esbuild src/index.ts --bundle --platform=node --outdir=dist
Définition 35 (Transpilateur TypeScript)
Un transpilateur TypeScript est un outil qui transforme du code TypeScript en JavaScript sans effectuer de vérification de types complète. tsx et esbuild sont des transpilateurs : ils suppriment les annotations de types et émettent du JavaScript rapidement, mais ne signalent pas les erreurs de types. tsc est le compilateur officiel : il vérifie les types et émet du JavaScript. En développement, on utilise souvent un transpilateur pour la vitesse et tsc --noEmit séparément pour la vérification.
Les types Node.js : @types/node#
Node.js expose un grand nombre d’API spécifiques à l’environnement serveur (fs, path, http, process, etc.) qui n’existent pas dans le navigateur. Le paquet @types/node fournit les déclarations TypeScript correspondantes.
npm install --save-dev @types/node
Une fois installé, tous les modules natifs Node.js sont typés :
import fs from "fs";
import path from "path";
import http from "http";
import { EventEmitter } from "events";
import { Readable, Writable } from "stream";
import crypto from "crypto";
import os from "os";
Modules principaux typés#
import path from "path";
// Toutes les méthodes sont typées avec précision
const cheminAbsolu: string = path.resolve("src", "index.ts");
const extension: string = path.extname("fichier.ts"); // ".ts"
const répertoire: string = path.dirname("/src/utils/format.ts");
const nomFichier: string = path.basename("/src/utils/format.ts", ".ts");
// path.join est variadique et retourne toujours string
const chemin: string = path.join("src", "services", "utilisateur.ts");
import { createServer, IncomingMessage, ServerResponse } from "http";
// IncomingMessage et ServerResponse sont des interfaces complètes
const serveur = createServer((req: IncomingMessage, res: ServerResponse) => {
const url: string = req.url ?? "/";
const méthode: string = req.method ?? "GET";
const en_têtes: Record<string, string | string[] | undefined> = req.headers;
});
// process est disponible globalement grâce à @types/node
const plateforme: NodeJS.Platform = process.platform;
const pid: number = process.pid;
const argv: string[] = process.argv;
const env: NodeJS.ProcessEnv = process.env;
// NodeJS.ProcessEnv type toutes les valeurs en string | undefined
const port = process.env.PORT; // string | undefined
Gestion des chemins#
__dirname et __filename en CommonJS#
En mode CommonJS, Node.js injecte __dirname (répertoire du fichier courant) et __filename (chemin absolu du fichier courant) comme variables globales :
import path from "path";
// Chemin absolu vers un fichier de configuration dans le même répertoire
const cheminConfig = path.join(__dirname, "config.json");
// Remonter d'un répertoire
const racineProjet = path.resolve(__dirname, "..");
Équivalents ESM avec import.meta.url#
En mode ESM natif, __dirname et __filename ne sont pas disponibles. On les reconstruit à partir de import.meta.url :
import { fileURLToPath } from "url";
import path from "path";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Ou, plus concis avec la classe URL :
const cheminConfig = new URL("./config.json", import.meta.url).pathname;
Remarque 28
Le type de import.meta.url est string dans TypeScript, mais il n’est disponible que lorsque module est configuré sur "ES2020" ou supérieur dans tsconfig.json. Pour les projets CommonJS, import.meta n’existe pas et tentera de __dirname/__filename directement. Les projets hybrides (bibliothèques supportant les deux modes) doivent gérer cette différence explicitement.
HTTP avec typage fort#
Serveur HTTP natif#
import { createServer, IncomingMessage, ServerResponse } from "http";
interface Réponse<T> {
succès: boolean;
données?: T;
erreur?: string;
}
function répondreJson<T>(res: ServerResponse, code: number, corps: Réponse<T>): void {
res.writeHead(code, { "Content-Type": "application/json; charset=utf-8" });
res.end(JSON.stringify(corps));
}
const serveur = createServer((req: IncomingMessage, res: ServerResponse) => {
const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
if (req.method === "GET" && url.pathname === "/santé") {
répondreJson(res, 200, { succès: true, données: { statut: "ok" } });
} else {
répondreJson(res, 404, { succès: false, erreur: "Route inconnue" });
}
});
serveur.listen(3000, () => {
console.log("Serveur démarré sur http://localhost:3000");
});
Express.js avec @types/express#
Express est la bibliothèque HTTP la plus répandue dans l’écosystème Node.js. Avec @types/express, toutes les interfaces Request, Response et NextFunction sont complètement typées.
npm install express
npm install --save-dev @types/express
import express, { Request, Response, NextFunction, Router } from "express";
// Étendre Request pour inclure l'utilisateur authentifié
interface RequêteAuthentifiée extends Request {
utilisateur: {
id: string;
rôles: string[];
};
}
// Middleware d'authentification
function authentifier(
req: RequêteAuthentifiée,
res: Response,
next: NextFunction
): void {
const jeton = req.headers.authorization?.replace("Bearer ", "");
if (!jeton) {
res.status(401).json({ erreur: "Jeton manquant" });
return;
}
// Dans un vrai projet : vérifier le JWT
req.utilisateur = { id: "usr_123", rôles: ["admin"] };
next();
}
// Routeur fortement typé
const routeurUtilisateurs: Router = Router();
routeurUtilisateurs.get(
"/",
async (_req: Request, res: Response): Promise<void> => {
const utilisateurs = [{ id: "1", nom: "Alice" }, { id: "2", nom: "Bob" }];
res.json({ succès: true, données: utilisateurs });
}
);
routeurUtilisateurs.post(
"/",
async (req: Request<{}, {}, { nom: string; email: string }>, res: Response): Promise<void> => {
const { nom, email } = req.body;
// req.body est typé comme { nom: string; email: string }
res.status(201).json({ succès: true, données: { id: "3", nom, email } });
}
);
const app = express();
app.use(express.json());
app.use("/utilisateurs", routeurUtilisateurs);
app.listen(3000);
Exemple 13 (API REST minimale complète)
Une mini API de gestion de tâches illustrant les patterns TypeScript + Express :
import express, { Request, Response } from "express";
interface Tâche {
id: number;
titre: string;
terminée: boolean;
créée: Date;
}
const tâches: Tâche[] = [];
let compteur = 1;
const app = express();
app.use(express.json());
// GET /tâches — lister toutes les tâches
app.get("/tâches", (_req: Request, res: Response<Tâche[]>) => {
res.json(tâches);
});
// POST /tâches — créer une tâche
app.post(
"/tâches",
(req: Request<{}, Tâche, { titre: string }>, res: Response<Tâche>) => {
const nouvelle: Tâche = {
id: compteur++,
titre: req.body.titre,
terminée: false,
créée: new Date(),
};
tâches.push(nouvelle);
res.status(201).json(nouvelle);
}
);
app.listen(3000, () => console.log("API démarrée"));
## Accès aux fichiers
Le module `fs` de Node.js propose depuis longtemps une API à base de callbacks. L'API `fs.promises` expose les mêmes opérations sous forme de `Promise`, parfaitement adaptées à `async`/`await`.
```typescript
import fs from "fs/promises";
import path from "path";
// Lire un fichier texte
async function lireFichier(cheminFichier: string): Promise<string> {
const contenu = await fs.readFile(cheminFichier, "utf-8");
return contenu;
}
// Écrire un fichier en créant les répertoires intermédiaires si nécessaire
async function écrireFichier(cheminFichier: string, contenu: string): Promise<void> {
await fs.mkdir(path.dirname(cheminFichier), { recursive: true });
await fs.writeFile(cheminFichier, contenu, "utf-8");
}
// Lister les fichiers d'un répertoire avec filtrage par extension
async function listerFichiersTs(répertoire: string): Promise<string[]> {
const entrées = await fs.readdir(répertoire, { withFileTypes: true });
return entrées
.filter(e => e.isFile() && e.name.endsWith(".ts"))
.map(e => path.join(répertoire, e.name));
}
// Traitement parallèle de plusieurs fichiers
async function traiterFichiers(répertoire: string): Promise<void> {
const fichiers = await listerFichiersTs(répertoire);
const contenus = await Promise.all(fichiers.map(lireFichier));
for (const [index, contenu] of contenus.entries()) {
console.log(`${fichiers[index]} : ${contenu.length} caractères`);
}
}
Remarque 29
Toutes les fonctions de fs.promises peuvent lancer des exceptions de type NodeJS.ErrnoException, une sous-interface de Error qui ajoute les champs code (ex. "ENOENT", "EACCES"), errno et path. TypeScript ne contraint pas l’usage d’un bloc try/catch — c’est à vous de gérer les erreurs. Un pattern robuste consiste à créer des fonctions enveloppantes qui retournent un Result<T, E> ou à utiliser un utilitaire comme neverthrow pour les erreurs prévisibles.
Variables d’environnement#
process.env et NodeJS.ProcessEnv#
process.env est typé comme NodeJS.ProcessEnv, ce qui correspond à Record<string, string | undefined>. En conséquence, toute valeur lue depuis process.env peut être undefined — TypeScript vous force à le gérer.
// Mauvaise pratique — TypeScript signale l'erreur
const port: number = process.env.PORT; // Erreur : string | undefined n'est pas assignable à number
// Pratique correcte — conversion et valeur par défaut
const port: number = parseInt(process.env.PORT ?? "3000", 10);
const hôte: string = process.env.HOST ?? "0.0.0.0";
const estProduction: boolean = process.env.NODE_ENV === "production";
Validation avec zod#
Pour les applications sérieuses, valider les variables d’environnement au démarrage évite des erreurs difficiles à diagnostiquer en production. La bibliothèque zod est particulièrement bien adaptée :
npm install zod
import { z } from "zod";
// Schéma de validation des variables d'environnement
const schémaEnv = z.object({
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
PORT: z.coerce.number().int().min(1).max(65535).default(3000),
DATABASE_URL: z.string().url("DATABASE_URL doit être une URL valide"),
JWT_SECRET: z.string().min(32, "JWT_SECRET doit faire au moins 32 caractères"),
LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
CORS_ORIGINS: z.string().transform(s => s.split(",")).default("http://localhost:3000"),
});
// Valider au démarrage — l'application s'arrête si la configuration est invalide
function chargerEnv() {
const résultat = schémaEnv.safeParse(process.env);
if (!résultat.success) {
console.error("Configuration d'environnement invalide :");
console.error(résultat.error.flatten().fieldErrors);
process.exit(1);
}
return résultat.data;
}
// Export typé — les types sont inférés du schéma zod
export const env = chargerEnv();
// Utilisation — tous les types sont corrects
const portServeur: number = env.PORT; // number, jamais undefined
const urlBd: string = env.DATABASE_URL; // string valide
const origines: string[] = env.CORS_ORIGINS; // string[] (déjà transformé)
Définition 36 (Validation de schéma au démarrage)
La validation de schéma au démarrage (startup schema validation) est la pratique qui consiste à vérifier la validité de la configuration de l’application (variables d’environnement, fichiers de configuration) au moment du lancement, avant d’accepter la moindre requête. Si la configuration est incorrecte, l’application s’arrête immédiatement avec un message d’erreur clair. Cette pratique suit le principe fail fast et évite des erreurs tardives et obscures en production.
Compilation et déploiement#
Compiler pour la production avec tsc#
# Compiler une fois — génère dist/ à partir de src/
npx tsc
# Vérifier les types sans émettre de fichiers (utile en CI)
npx tsc --noEmit
# Mode surveillance — recompile à chaque modification
npx tsc --watch
Pour s’assurer que le répertoire dist/ est propre avant chaque build, on ajoute un script de nettoyage :
{
"scripts": {
"clean": "rm -rf dist",
"build": "npm run clean && tsc",
"build:check": "tsc --noEmit",
"start": "node dist/index.js",
"dev": "tsx watch src/index.ts",
"prod": "npm run build && npm start"
}
}
Optimisations courantes pour la production#
# esbuild — compilation ultra-rapide avec bundling
npx esbuild src/index.ts \
--bundle \
--platform=node \
--target=node20 \
--outfile=dist/index.js \
--external:express \
--external:zod
# ncc (Vercel) — crée un exécutable Node.js autonome sans node_modules
npx @vercel/ncc build src/index.ts -o dist --minify
Exemple de Dockerfile pour une API TypeScript#
# Étape 1 : compilation
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json tsconfig.json ./
RUN npm ci
COPY src/ ./src/
RUN npm run build
# Étape 2 : image de production minimale
FROM node:20-alpine AS production
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=builder /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/index.js"]
Exemple 14 (Scripts npm complets pour un projet Node.js + TypeScript)
Un ensemble de scripts npm couvrant l’ensemble du cycle de vie du projet :
{
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc -p tsconfig.json",
"build:fast": "esbuild src/index.ts --bundle --platform=node --outdir=dist",
"start": "node dist/index.js",
"typecheck": "tsc --noEmit",
"lint": "eslint src --ext .ts --max-warnings 0",
"lint:fix": "eslint src --ext .ts --fix",
"format": "prettier --write \"src/**/*.ts\"",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"ci": "npm run typecheck && npm run lint && npm run test",
"prepublishOnly": "npm run ci && npm run build"
}
}
```{code-cell} python
:tags: [hide-input]
fig, ax = plt.subplots(figsize=(14, 9))
ax.set_xlim(0, 14)
ax.set_ylim(0, 10)
ax.axis('off')
ax.set_title('Architecture d\'un projet Node.js + TypeScript', fontsize=16,
fontweight='bold', pad=20)
palette = sns.color_palette("muted", 8)
bleu = palette[0]
orange = palette[1]
vert = palette[2]
rouge = palette[3]
violet = palette[4]
# ---- Répertoire src/ ----
src_box = patches.FancyBboxPatch(
(0.3, 4.2), 3.8, 5.2,
boxstyle="round,pad=0.2", linewidth=2.5,
edgecolor=bleu, facecolor=bleu, alpha=0.12)
ax.add_patch(src_box)
ax.text(2.2, 9.1, 'src/', ha='center', va='center',
fontsize=14, fontweight='bold', color=bleu,
fontfamily='monospace')
fichiers_src = [
'index.ts',
'routes/',
'services/',
'modèles/',
'types/',
'utils/',
]
for i, nom in enumerate(fichiers_src):
fy = 8.35 - i * 0.72
est_dossier = nom.endswith('/')
couleur_f = bleu if est_dossier else '#555555'
ax.text(0.85, fy, ('📁 ' if est_dossier else '📄 ') + nom,
ha='left', va='center', fontsize=9.5,
color=couleur_f, fontfamily='monospace')
# ---- Répertoire node_modules/ ----
nm_box = patches.FancyBboxPatch(
(0.3, 0.5), 3.8, 3.3,
boxstyle="round,pad=0.2", linewidth=2.5,
edgecolor=orange, facecolor=orange, alpha=0.12)
ax.add_patch(nm_box)
ax.text(2.2, 3.55, 'node_modules/', ha='center', va='center',
fontsize=12, fontweight='bold', color=orange,
fontfamily='monospace')
paquets = [
'@types/node/',
'@types/express/',
'express/',
'zod/',
'typescript/',
]
for i, nom in enumerate(paquets):
py = 2.95 - i * 0.48
ax.text(0.75, py, '📦 ' + nom, ha='left', va='center',
fontsize=8.5, color='#555555', fontfamily='monospace')
# ---- Compilateur tsc / tsx ----
comp_x, comp_y = 5.3, 5.6
comp_box = patches.FancyBboxPatch(
(comp_x, comp_y), 3.4, 2.2,
boxstyle="round,pad=0.2", linewidth=2.5,
edgecolor=violet, facecolor=violet, alpha=0.12)
ax.add_patch(comp_box)
ax.text(comp_x + 1.7, comp_y + 1.75, 'Compilateur', ha='center', va='center',
fontsize=12, fontweight='bold', color=violet)
ax.text(comp_x + 1.7, comp_y + 1.2,
'tsc → production', ha='center', va='center',
fontsize=9, color='#555555', fontfamily='monospace')
ax.text(comp_x + 1.7, comp_y + 0.75,
'tsx → développement', ha='center', va='center',
fontsize=9, color='#555555', fontfamily='monospace')
ax.text(comp_x + 1.7, comp_y + 0.3,
'esbuild → bundle rapide', ha='center', va='center',
fontsize=9, color='#555555', fontfamily='monospace')
# ---- Répertoire dist/ ----
dist_box = patches.FancyBboxPatch(
(10.0, 4.2), 3.6, 5.2,
boxstyle="round,pad=0.2", linewidth=2.5,
edgecolor=vert, facecolor=vert, alpha=0.12)
ax.add_patch(dist_box)
ax.text(11.8, 9.1, 'dist/', ha='center', va='center',
fontsize=14, fontweight='bold', color=vert,
fontfamily='monospace')
fichiers_dist = [
'index.js',
'index.d.ts',
'index.js.map',
'routes/',
'services/',
'modèles/',
]
for i, nom in enumerate(fichiers_dist):
fy = 8.35 - i * 0.72
est_dossier = nom.endswith('/')
couleur_f = vert if est_dossier else '#555555'
ax.text(10.35, fy, ('📁 ' if est_dossier else '📄 ') + nom,
ha='left', va='center', fontsize=9.5,
color=couleur_f, fontfamily='monospace')
# ---- Node.js runtime ----
rt_box = patches.FancyBboxPatch(
(10.0, 0.5), 3.6, 3.3,
boxstyle="round,pad=0.2", linewidth=2.5,
edgecolor=rouge, facecolor=rouge, alpha=0.12)
ax.add_patch(rt_box)
ax.text(11.8, 3.55, 'Node.js runtime', ha='center', va='center',
fontsize=12, fontweight='bold', color=rouge)
ax.text(11.8, 2.9, 'node dist/index.js', ha='center', va='center',
fontsize=9, color='#555555', fontfamily='monospace',
bbox=dict(boxstyle='round,pad=0.2', facecolor='white',
edgecolor=rouge, alpha=0.6))
lignes_rt = ['V8 engine', 'libuv (I/O asynchrone)', 'Modules CommonJS/ESM']
for i, ligne in enumerate(lignes_rt):
ax.text(11.8, 2.25 - i * 0.55, ligne, ha='center', va='center',
fontsize=8.5, color='#666666')
# ---- Flèches ----
# src → compilateur
ax.annotate('', xy=(comp_x, comp_y + 1.1), xytext=(4.1, 6.7),
arrowprops=dict(arrowstyle='->', color='#444444', lw=2.2))
ax.text(4.9, 7.0, '.ts → .js', ha='center', va='center',
fontsize=9, color='#555555', style='italic')
# compilateur → dist
ax.annotate('', xy=(10.0, comp_y + 1.1), xytext=(comp_x + 3.4, comp_y + 1.1),
arrowprops=dict(arrowstyle='->', color='#444444', lw=2.2))
# dist → runtime
ax.annotate('', xy=(11.8, 3.8), xytext=(11.8, 4.2),
arrowprops=dict(arrowstyle='->', color=rouge, lw=2,
linestyle='dashed'))
# tsconfig.json
ax.text(7.0, 8.6, 'tsconfig.json', ha='center', va='center',
fontsize=10, fontweight='bold', color='#333333',
fontfamily='monospace',
bbox=dict(boxstyle='round,pad=0.3', facecolor='#fffbe6',
edgecolor='#ccaa00', linewidth=1.5))
ax.annotate('', xy=(comp_x + 1.7, comp_y + 2.2), xytext=(7.0, 8.35),
arrowprops=dict(arrowstyle='->', color='#ccaa00',
lw=1.5, linestyle='dotted'))
plt.tight_layout()
plt.show()
Résumé#
Dans ce chapitre, nous avons couvert l’intégration complète de TypeScript dans l’écosystème Node.js :
La structure recommandée sépare le code source (
src/) du code compilé (dist/) et centralise la configuration danstsconfig.json. La configuration diffère selon le mode de modules choisi :CommonJS(historique) ouNodeNext/ESM(moderne).Le paquet
@types/nodefournit les déclarations de types pour tous les modules natifs Node.js. Les modulesfs,path,httpetprocesssont entièrement typés.La gestion des chemins de fichiers diffère entre CommonJS (
__dirname,__filename) et ESM (import.meta.url+fileURLToPath).Avec
@types/express, les interfacesRequest,ResponseetNextFunctionsont précisément typées. On peut étendreRequestpour ajouter des propriétés personnalisées (utilisateur authentifié, identifiant de corrélation).L’API
fs.promisesavecasync/awaitoffre une gestion asynchrone des fichiers propre et entièrement typée.Les variables d’environnement sont typées comme
string | undefinedpar défaut. La validation aveczodau démarrage est la pratique recommandée pour garantir que la configuration est correcte avant d’accepter des requêtes.Pour la compilation et le déploiement,
tscproduit le JavaScript de production,tsxouts-nodeservent au développement, etesbuildpermet des compilations ultra-rapides avec bundling.tsc --noEmitest utilisé en intégration continue pour la vérification des types.
Les chapitres suivants aborderont l’utilisation de TypeScript avec les frameworks front-end : React (chapitre 13) et Vue.js (chapitre 14), qui exploitent les mêmes fondamentaux mais dans un contexte de rendu côté client.