Gérer l’upload de fichiers sous React Native vers une API Node.js avec Axios et Multer
Dans cet article nous allons voir comment émettre un fichier depuis une application mobile développée sous React Native vers une API Node.js dont la gestion des fichiers uploadés est gérée par le middleware Multer. Les requêtes vers notre API depuis notre application mobile seront effectuées à l’aide de la librairie axios.
Cet article part du principe que vous avez déjà un environnement de développement Mobile installé sur votre poste. Dans le cas contraire veuillez vous référer à la documentation de React Native.
Définition de l’API sous Node.js
Notre API est assez simple dans sa structure et se repose sur express.js pour la mise en place du serveur Web. Elle propose un seul endpoint qui se chargera de sauvegarder sur le serveur le fichier émit depuis notre application mobile via React Native. On pourrait se charger de réceptionner et de sauvegarder le buffer de données à l’aide du module file system (fs) de Node.js, mais il existe un autre module pour faciliter l’upload de fichier : Multer.
Si vous êtes novice dans l’utilisation de Node.js, je vous invite à lire les articles suivants qui introduisent cet outil :
– Premiers pas avec Node.js
– Initialiser un projet Node.js sous TypeScript et ESLint
– Initialiser un projet React sous TypeScript avec Webpack
Multer a cependant une contrainte, la requête d’envoie contenant le fichier doit être de la forme multipart/form-data
. Mais nous verrons ça un peu plus tard au moment d’expliquer la partie React Native.
Pour initialiser notre API commencez par exécuter la commande suivante dans le dossier devant contenir votre projet Node.js :
npm init -y
Vous devrez ensuite installer l’ensemble des modules suivants :
npm i --save express@4.17.1 cors@2.8.5 multer@1.4.4
Vous n’êtes pas obligé de préciser le numéro de version de chaque module, mais pour être sûr que notre exemple fonctionne, autant vous indiquer celles utilisées lors de la rédaction de cet article.
Une fois l’installation terminée, il ne vous reste plus qu’à créer un fichier index.js
à la racine de votre projet et d’y coller le code suivant :
const express = require("express");
const cors = require("cors");
const multer = require("multer");
const app = express();
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, "upload");
},
filename: function (req, file, cb) {
cb(null, file.originalname);
},
});
const upload = multer({ storage: storage });
app.use(cors());
app.use(express.json());
app.post("/api/saveimg", upload.single("picture"), (req, res, next) => {
const file = req.file;
if (!file) {
const error = new Error("Please upload a file");
error.httpStatusCode = 400;
return next(error);
}
res.status(200).send(file);
});
app.listen(8000, () => {
console.log("server listening on port 8000");
});
J’ai pris ici la liberté de configurer un peu plus ma gestion des fichiers sous Multer en utilisant la méthode d’initialisation diskStorage
:
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, "upload");
},
filename: function (req, file, cb) {
cb(null, file.originalname);
},
});
Ca permet de définir le dossier de destination (destination
) ainsi que le nom du fichier (filename
) stocké sur le serveur. Attention cependant, sous cette forme Multer ne prends pas en charge la création du dossier. Veillez à bien créer le dossier upload
(ou tout autre nom que vous voulez utiliser) à la racine de votre projet.
Si vous n’avez pas besoin de configurer plus profondément votre gestion des fichiers, vous pouvez simplement instancier la variable upload
de cette manière et omettre la variable storage
:
const upload = multer({ dest: 'upload/'});
L’instance de Multer (upload
) se passe dans les paramètres de route du endpoint de notre serveur express.js :
app.post("/api/saveimg", upload.single("picture"), (req, res, next) => { ...
Multer se chargera alors de récupérer, depuis la requête cliente, le fichier et de le sauvegarder dans le dossier paramétré. Vous aurez ensuite la possibilité de consulter, dans le callback du endpoint, le contenu du fichier via la propriété req.file
. N’hésitez pas à regarder la documentation de Multer pour connaitre toutes les fonctions disponibles. Ici, j’utilise la méthode single
pour ne récupérer que le champ picture
contenu dans la requête cliente et que nous allons définir de ce pas dans notre application mobile sous React Native.
Prendre une photo et l’envoyer vers l’API
Initialisation du projet
Première chose à faire initialiser un nouveau projet grâce à la CLI de React Native :
npx react-native init takepicturefromcamera
Mon application mobile aura pour simple but de prendre une photo avec la caméra de l’appareil et la transmettre à mon API Node.js.
Avant d’aller plus loin vous devez installer les modules suivants dans le projet :
npm i --save axios@0.27.2 react-native-camera@4.2.1 react-native-vector-icons@9.1.0 urijs@1.19.11
Vous aurez également besoin d’installer comme dépendances de développement des déclarations liées à TypeScript pour certains des modules ci-dessus :
npm i -D @types/react-native-vector-icons @types/urijs
React Native Camera
Dans mon exemple j’utilise le composant RNCamera
de React-Native-Camera pour afficher et générer une prise de vue depuis la caméra du mobile.
Nous allons déjà créer un composant Camera
qui contiendra une définition du module RNCamera
et que nous importerons ensuite dans notre fichier App.tsx
nouvellement créé. Créez un dossier components
à la racine du projet et ajoutez un fichier Camera.tsx
qui contient le code suivant :
import React, {forwardRef} from 'react';
import {StyleSheet} from 'react-native';
import {RNCamera} from 'react-native-camera';
const Camera = forwardRef<RNCamera>((props, ref) => {
return (
<RNCamera
ref={ref}
captureAudio={false}
style={styles.rnCamera}
type={RNCamera.Constants.Type.back}
ratio={'4:3'}
flashMode={RNCamera.Constants.FlashMode.off}
androidCameraPermissionOptions={{
title: 'Permission to use camera',
message: 'We need your permission to use your camera',
buttonPositive: 'Ok',
buttonNegative: 'Cancel',
}}
/>
);
});
const styles = StyleSheet.create({
rnCamera: {
flex: 1,
width: '90%',
height: '90%',
overflow: 'hidden',
justifyContent: 'flex-end',
alignSelf: 'center',
alignItems: 'center',
},
});
export default Camera;
Application
Allez ensuite dans le fichier App.tsx
à la racine du projet et remplacez en intégralité le code par celui ci-dessous :
import React, {ReactNode, useRef} from 'react';
import {Platform} from 'react-native';
import Camera from './components/Camera';
import Icon from 'react-native-vector-icons/FontAwesome';
import axios from 'axios';
import URI from 'urijs';
import {
SafeAreaView,
StyleSheet,
Text,
useColorScheme,
View,
TouchableOpacity,
Alert,
} from 'react-native';
import {RNCamera} from 'react-native-camera';
import {Colors} from 'react-native/Libraries/NewAppScreen';
const styles = StyleSheet.create({
sectionContainer: {
marginTop: 32,
paddingHorizontal: 24,
},
sectionTitle: {
fontSize: 24,
fontWeight: '600',
},
sectionDescription: {
marginTop: 8,
fontSize: 18,
fontWeight: '400',
},
highlight: {
fontWeight: '700',
},
screen: {
flex: 1,
backgroundColor: '#F2F2FC',
},
saveArea: {
backgroundColor: '#62d1bc',
},
topBar: {
height: 50,
backgroundColor: '#62d1bc',
alignItems: 'center',
justifyContent: 'center',
},
topBarTitleText: {
color: '#ffffff',
fontSize: 20,
},
caption: {
height: 120,
justifyContent: 'center',
alignItems: 'center',
},
captionTitleText: {
color: '#121B0D',
fontSize: 16,
fontWeight: '600',
},
btn: {
width: 240,
borderRadius: 4,
backgroundColor: '#62d1bc',
paddingHorizontal: 24,
paddingVertical: 12,
marginVertical: 8,
},
btnText: {
fontSize: 18,
color: '#ffffff',
textAlign: 'center',
},
rnCamera: {
flex: 1,
width: '94%',
alignSelf: 'center',
},
rmCameraResult: {
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#eeeeee',
},
rmCameraResultText: {
fontSize: 20,
color: '#62d1bc',
},
cameraControl: {
height: 180,
justifyContent: 'center',
alignItems: 'center',
},
});
const Section: React.FC<{
children: ReactNode;
title: string;
}> = ({children, title}) => {
const isDarkMode = useColorScheme() === 'dark';
return (
<View style={styles.sectionContainer}>
<Text
style={[
styles.sectionTitle,
{
color: isDarkMode ? Colors.white : Colors.black,
},
]}>
{title}
</Text>
<Text
style={[
styles.sectionDescription,
{
color: isDarkMode ? Colors.light : Colors.dark,
},
]}>
{children}
</Text>
</View>
);
};
const App = () => {
const isDarkMode = useColorScheme() === 'dark';
const backgroundStyle = {
backgroundColor: isDarkMode ? Colors.darker : Colors.lighter,
};
const camera = useRef<RNCamera>(null);
const takePicture = async (): Promise<void> => {
const options = {
quality: 0.5,
base64: true,
fixOrientation: true,
forceUpOrientation: true,
};
camera?.current
?.takePictureAsync(options)
.then(response => {
const formData = new FormData();
formData.append('picture', {
name: new URI(response.uri).filename(),
type: 'image/jpeg',
uri: Platform.OS !== 'android' ? 'file://' + response.uri : response.uri
});
axios.post('http://10.0.2.2:8000/api/saveimg', formData, {
headers: { 'Content-type': 'multipart/form-data' },
transformRequest: (data: FormData) => {
return data;
}
})
.then(() => {
Alert.alert('Success', 'Image uploaded !');
});
})
.catch((err: unknown) => {
if (typeof err === 'string') {
Alert.alert('Error', 'Failed to take picture: ' + err.toUpperCase());
} else if (err instanceof Error) {
Alert.alert(
'Error',
'Failed to take picture: ' + (err.message || err),
);
}
throw err;
});
};
return (
<View style={styles.screen}>
<SafeAreaView style={backgroundStyle}>
<View style={styles.topBar}>
<Text style={styles.topBarTitleText}>Deep Fake</Text>
</View>
</SafeAreaView>
<View style={styles.caption}>
<Section title="Tips">
Place your face front of the camera and take a screenshot.
</Section>
</View>
<Camera ref={camera} />
<View style={styles.cameraControl}>
<TouchableOpacity onPress={takePicture}>
<Icon name="camera" size={50} color="#62d1bc" />
</TouchableOpacity>
</View>
</View>
);
};
export default App;
J’ai transformé une partie du code fournit par la template par défaut de React Native pour qu’elle corresponde à mes besoins. Je ne vais pas détailler le code de l’application, cet article n’étant pas dédié spécifiquement à React Native (un peu quand même), par contre je vais m’attarder sur ce qui nous intéresse, à savoir l’appel à notre API.
Axios
RNCamera
fournit une méthode asynchrone takePictureAsync
qui retourne une promesse dont la réponse contient l’URI d’accès où est stockée la photo prise avec la caméra. C’est également à ce niveau que j’effectue l’appel à mon API à l’aide de la librairie axios :
const formData = new FormData();
formData.append('picture', {
name: new URI(response.uri).filename(),
type: 'image/jpeg',
uri: Platform.OS !== 'android' ? 'file://' + response.uri : response.uri
});
axios.post('http://10.0.2.2:8000/api/saveimg', formData, {
headers: {'Content-type': 'multipart/form-data'},
transformRequest: (data: FormData) => { return data; }
}).then(() => {
Alert.alert('Success', 'Image uploaded !');
});
Premier point important, Multer ne prends en charge que les requêtes dont le contenu est de type multipart/form-data
. Il est donc nécessaire de préciser dans la configuration des headers de la requête l’attribut Content-type
sur multipart/form-data
.
L’autre point important est la structure du format des données à envoyer à l’API et surtout l’utilisation de l’objet FormData
fournit par React Native (et non le module form-data) pour encapsuler les données à émettre. Celui-ci doit contenir à minima 3 attributs qui sont respectivement :
name
: le nom du fichier émit.type
: le type MIME du fichier émit.uri
: l’URI d’accès au fichier émit.
Notez également le nom du champ utilisé pour regrouper les informations du formulaire : picture
. C’est bien celui que nous utilisons dans l’API Node.js via l’instance de Multer pour traiter les informations reçues par la requête.
Le dernier point, et probablement le plus important, c’est qu’axios converti par défaut le formulaire en string. Il est donc impératif de forcer axios à envoyer FormData
sous le format d’origine sinon il ne sera pas interprété par Multer en arrivant dans l’API. C’est là qu’intervient transformRequest
qui permet de surcharger la requête avec les données au bon format.
Exécution
C’est un peu bête à dire, mais c’est probablement l’une des étapes les plus compliquée lorsqu’on souhaite tester une application mobile sur son poste. Personnellement, j’utilise Visual Studio Code pour développer mon application, mais comble de malchance, je développe sous Windows. Ce qui élimine déjà la possibilité de tester sous iOS, Apple ayant verrouillé l’utilisation de XCode, son éditeur mobile, sur son parc de machine.
Reste Android et Android Studio qui permet l’installation de l’émulateur Android sur les postes Windows. Avec l’intégration de React Native et Dart (le langage utilisé par Flutter, le concurrent direct de React Native) dans Visual Studio Code, il est alors possible de lancer un émulateur Android depuis la barre de statut de Visual Studio Code, puis de lancer la commande :
npm run android
VSCode va alors se charger de lancer une instance de Metro qui sert de connecteur entre notre application React Native et l’émulateur Android. Vous aurez alors la possibilité de tester localement votre application Mobile, de réaliser des modifications à chaud et d’en voir les effets directement dans l’émulateur.
Conclusion
Dans cet article nous avons vu comment transmettre un fichier depuis une application mobile, développée avec le Framework React Native de Facebook, vers une API sous Node.js. Nous avons décortiqué l’utilisation de la librairie axios pour l’émission de la requête, et vu de quelle manière elle est interceptée et traitée par le module Multer d’express.js.
Pour les plus curieux retrouvez la même chose mais cette fois avec Flutter :
https://developer.gutsfun.com/gerer-upload-fichiers-flutter-nodejs-http-multer/
Tips
Petite aparté, vous aurez peut-être remarqué l’URL du endpoint utilisé pour contacter l’API : http://10.0.2.2:8000/api/saveimg
. 10.0.2.2
correspond en réalité à l’adresse local de l’émulateur Android fournit par Android Studio et redirigé ensuite vers le localhost
de mon poste.
Notez également que pour faire fonctionner correctement l’application sur Android ou iOS vous aurez probablement besoin de modifier les autorisations d’accès à certains composants comme la caméra. Référez vous toujours à la documentation des modules que vous installez pour connaître les autorisations nécessaires à leur bon fonctionnement.
Références
- Dépôt Github contenant les sources de l’article : https://github.com/GutsFun/Upload-File-To-API-With-ReactNative-Multer-Axios
- Site officiel de React Native : https://reactnative.dev/
- Site officiel de Node.js : https://nodejs.org/
- GitHub de Multer : https://github.com/expressjs/multer
- GitHub d’express.js : https://github.com/expressjs/express
- GitHub d’axios : https://github.com/axios/axios
- Site officiel de React-Native-Camera : https://react-native-camera.github.io/react-native-camera/