Une API REST pour l'application CultureQuiz, un système de quiz interactif permettant de tester vos connaissances culturelles par catégorie. Cette API complète gère les catégories, questions, réponses, comptes utilisateurs et l'historique des quiz.
Version: 1.0.0
Stack: Node.js + Express + PostgreSQL
Authentification: JWT (JSON Web Tokens)
- Installation et démarrage
- Configuration
- Architecture et endpoints
- Guide d'authentification
- Intégration avec React
- Exemples d'utilisation
- Docker
- Troubleshooting
- Node.js (v14+)
- PostgreSQL (v13+) ou Docker
- npm ou yarn
# 1. Cloner le repository
git clone <votre-repo>
cd CultureQuiz_API
# 2. Installer les dépendances
npm install
# 3. Créer un fichier .env
cp .env.example .env
# 4. Configurer les variables (voir section Configuration)
# 5. Initialiser la base de données
# Option A : Avec Docker (recommandé)
docker-compose up -d
# Option B : PostgreSQL local
psql -U postgres -d template1 -a -f db/db.sql
# 6. Démarrer l'API
npm start # Production
npm run dev # Développement (avec nodemon)L'API démarre sur http://localhost:3333
Créez un fichier .env à la racine du projet :
# Serveur
PORT=3333
NODE_ENV=development
# Base de données PostgreSQL
DB_HOST=localhost
DB_PORT=5432
DB_USER=culturequiz_user
DB_PASSWORD=culturequiz_password
DB_NAME=culturequiz
# Authentification JWT
JWT_SECRET=votre_cle_secrete_tres_longue_et_aleatoire
JWT_EXPIRATION=24h
# CORS
CORS_ORIGIN=http://localhost:3000Le service PostgreSQL inclus utilise :
- Utilisateur:
culturequiz_user - Mot de passe:
culturequiz_password - Base de données:
culturequiz - Port:
5432
/api
├── /categories (Catégories de quiz)
├── /questions (Questions avec différents niveaux)
├── /reponses (Réponses aux questions)
├── /comptes (Gestion des utilisateurs)
└── /historique (Historique des réponses)
GET /healthRéponse (200):
{
"status": "OK",
"message": "CultureQuiz API is running"
}GET /Réponse (200):
{
"message": "Bienvenue sur CultureQuiz API",
"version": "1.0.0",
"endpoints": {
"health": "/health",
"api": "/api",
"categories": "/api/categories",
"questions": "/api/questions",
"reponses": "/api/reponses",
"comptes": "/api/comptes",
"historique": "/api/historique"
}
}Gère les catégories de quiz disponibles.
GET /api/categoriesRéponse (200):
{
"success": true,
"data": [
{
"idCat": 1,
"libelleCategorie": "Histoire",
"diminutif": "HIS"
},
{
"idCat": 2,
"libelleCategorie": "Jeux Vidéo",
"diminutif": "JV"
}
]
}GET /api/categories/:idExemple: GET /api/categories/1
Réponse (200):
{
"success": true,
"data": {
"idCat": 1,
"libelleCategorie": "Histoire",
"diminutif": "HIS"
}
}POST /api/categories
Authorization: Bearer <token_jwt>
Content-Type: application/json
{
"libelleCategorie": "Littérature",
"diminutif": "LIT"
}Réponse (201):
{
"success": true,
"data": {
"idCat": 3,
"libelleCategorie": "Littérature",
"diminutif": "LIT"
}
}PUT /api/categories/:id
Authorization: Bearer <token_jwt>
Content-Type: application/json
{
"libelleCategorie": "Littérature française",
"diminutif": "LFR"
}DELETE /api/categories/:id
Authorization: Bearer <token_jwt>Gère les questions du quiz avec différents niveaux de difficulté.
GET /api/questionsRéponse (200):
{
"success": true,
"data": [
{
"idQue": 1,
"idCat": 1,
"libelleQue": "Qui a été le premier empereur romain?",
"niveau": 1,
"libelleCategorie": "Histoire"
}
]
}GET /api/questions/:idGET /api/questions/quiz/:idCatExemple: GET /api/questions/quiz/1
Réponse (200):
{
"success": true,
"data": {
"questions": [
{
"question": {
"idQue": 1,
"idCat": 1,
"libelleQue": "Qui a été le premier empereur romain?",
"niveau": 1,
"libelleCategorie": "Histoire"
},
"reponses": [
{
"idRep": 1,
"libelleRep": "Auguste",
"isBonneRep": true
},
{
"idRep": 2,
"libelleRep": "César",
"isBonneRep": false
},
{
"idRep": 3,
"libelleRep": "Néron",
"isBonneRep": false
},
{
"idRep": 4,
"libelleRep": "Julien",
"isBonneRep": false
}
]
}
],
"tempsLimite": 300,
"tempsParQuestion": 30,
"heureDebut": "2024-01-20T14:30:00Z"
}
}GET /api/questions/randomGET /api/questions/random/:idCatExemple: GET /api/questions/random/1
GET /api/questions/random/:idCat/:niveauExemple: GET /api/questions/random/1/2 (Catégorie 1, Niveau 2)
POST /api/questions/check-answer
Content-Type: application/json
{
"idRep": 12
}Réponses possibles (200):
- Réponse correcte :
{
"success": true,
"data": {
"isCorrect": true,
"selectedAnswer": {
"idRep": 12,
"libelleRep": "Auguste"
}
}
}- Réponse incorrecte (avec la bonne réponse retournée) :
{
"success": true,
"data": {
"isCorrect": false,
"selectedAnswer": {
"idRep": 13,
"libelleRep": "César"
},
"bonneReponse": {
"idRep": 12,
"libelleRep": "Auguste"
}
}
}Erreurs possibles :
400siidRepest manquant.404si aucune réponse ne correspond àidRep.
POST /api/questions
Authorization: Bearer <token_jwt>
Content-Type: application/json
{
"idCat": 1,
"libelleQue": "En quelle année la Révolution française a-t-elle commencé?",
"niveau": 2
}Réponse (201):
{
"success": true,
"data": {
"idQue": 42,
"idCat": 1,
"libelleQue": "En quelle année la Révolution française a-t-elle commencé?",
"niveau": 2
}
}PUT /api/questions/:id
Authorization: Bearer <token_jwt>
Content-Type: application/json
{
"libelleQue": "En quelle année exactement la Révolution française a-t-elle commencé?",
"niveau": 3
}DELETE /api/questions/:id
Authorization: Bearer <token_jwt>Gère les réponses aux questions (une question a 4 réponses possibles, une seule correcte).
GET /api/reponsesGET /api/reponses/question/:idQueExemple: GET /api/reponses/question/1
Réponse (200):
{
"success": true,
"count": 4,
"data": [
{
"idRep": 1,
"idQue": 1,
"libelleRep": "Auguste",
"isBonneRep": true
},
{
"idRep": 2,
"idQue": 1,
"libelleRep": "César",
"isBonneRep": false
},
{
"idRep": 3,
"idQue": 1,
"libelleRep": "Néron",
"isBonneRep": false
},
{
"idRep": 4,
"idQue": 1,
"libelleRep": "Julien",
"isBonneRep": false
}
]
}POST /api/reponses
Authorization: Bearer <token_jwt>
Content-Type: application/json
{
"idQue": 1,
"libelleRep": "Auguste",
"isBonneRep": true
}PUT /api/reponses/:id
Authorization: Bearer <token_jwt>
Content-Type: application/json
{
"libelleRep": "Octave Auguste",
"isBonneRep": true
}DELETE /api/reponses/:id
Authorization: Bearer <token_jwt>Gère la création de comptes utilisateurs et l'authentification.
POST /api/comptes/register
Content-Type: application/json
{
"pseudo": "john_doe",
"password": "SecurePassword123!"
}Réponse (201):
{
"success": true,
"data": {
"idCompte": 1,
"pseudo": "john_doe",
"dateCreation": "2024-01-20T10:30:00Z"
},
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}POST /api/comptes/login
Content-Type: application/json
{
"pseudo": "john_doe",
"password": "SecurePassword123!"
}Réponse (200):
{
"success": true,
"data": {
"idCompte": 1,
"pseudo": "john_doe",
"dateCreation": "2024-01-20T10:30:00Z",
"dateModif": "2024-01-20T10:30:00Z"
},
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}GET /api/comptes
Authorization: Bearer <token_jwt>GET /api/comptes/:id
Authorization: Bearer <token_jwt>PUT /api/comptes/:id
Authorization: Bearer <token_jwt>
Content-Type: application/json
{
"pseudo": "john_doe_updated",
"password": "NewPassword123!"
}DELETE /api/comptes/:id
Authorization: Bearer <token_jwt>Enregistre l'historique des résultats et des statistiques par partie et catégorie.
GET /api/historique
Authorization: Bearer <token_jwt>Réponse (200):
{
"success": true,
"count": 5,
"data": [
{
"idHistorique": 1,
"idCompte": 1,
"idCat": 1,
"score": 8,
"tempsJeu": 420,
"datePartie": "2024-01-20T14:30:00Z",
"libelleCategorie": "Histoire",
"pseudo": "john_doe"
}
]
}GET /api/historique/compte/:idCompte
Authorization: Bearer <token_jwt>Exemple: GET /api/historique/compte/1
Réponse (200):
{
"success": true,
"count": 5,
"totalQuestionsRepondues": 50,
"data": [
{
"idhistorique": 1,
"idcompte": 1,
"idcat": 1,
"score": 8,
"nbquestions": 10,
"tempsjeu": 120,
"datepartie": "2024-01-20T14:07:00Z",
"libellecategorie": "Histoire",
"pseudo": "john_doe"
}
]
}GET /api/historique/compte/:idCompte/categorie/:idCat
Authorization: Bearer <token_jwt>Exemple: GET /api/historique/compte/1/categorie/1
Réponse (200):
{
"success": true,
"count": 3,
"totalQuestionsRepondues": 30,
"data": [
{
"idhistorique": 1,
"idcompte": 1,
"idcat": 1,
"score": 9,
"nbquestions": 10,
"tempsjeu": 120,
"datepartie": "2024-01-20T14:07:00Z",
"libellecategorie": "Histoire",
"pseudo": "john_doe"
}
]
}GET /api/historique/stats/:idCompte
Authorization: Bearer <token_jwt>Réponse (200):
{
"success": true,
"data": {
"parCategorie": [
{
"idCat": 1,
"libelleCategorie": "Histoire",
"nombreParties": 10,
"totalQuestionsRepondues": 100,
"scoreMoyen": 7.5,
"meilleurScore": 10,
"pireScore": 4,
"tempsMoyen": 450,
"meilleurTemps": 300,
"tempsMaximum": 600
}
],
"statistiquesGlobales": {
"totalParties": 25,
"totalQuestionsRepondues": 250,
"scoreMoyenGlobal": 7.2,
"tempsMoyenGlobal": 420,
"tempsTotal": 10500
}
}
}POST /api/historique
Authorization: Bearer <token_jwt>
Content-Type: application/json
{
"idCompte": 1,
"idCat": 1,
"score": 8,
"nbQuestions": 10,
"heureDebut": "2024-01-20T14:00:00Z",
"heureFin": "2024-01-20T14:07:00Z"
}Réponse (201):
{
"success": true,
"data": {
"idHistorique": 42,
"idCompte": 1,
"idCat": 1,
"score": 8,
"nbQuestions": 10,
"tempsJeu": 420,
"datePartie": "2024-01-20T14:07:00Z"
}
}L'API utilise JWT (JSON Web Tokens) pour sécuriser certains endpoints.
Étape 1: S'inscrire
curl -X POST http://localhost:3333/api/comptes/register \
-H "Content-Type: application/json" \
-d '{
"pseudo": "john_doe",
"password": "SecurePassword123!"
}'Réponse:
{
"success": true,
"data": {
"idCompte": 1,
"pseudo": "john_doe",
"dateCreation": "2024-01-20T10:30:00Z"
},
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZENvbXB0ZSI6MSwicHNldWRvIjoiam9obl9kb2UiLCJpYXQiOjE3MDU3NTMwMDB9.xyz..."
}Étape 2: Ou se connecter
curl -X POST http://localhost:3333/api/comptes/login \
-H "Content-Type: application/json" \
-d '{
"pseudo": "john_doe",
"password": "SecurePassword123!"
}'Pour accéder à un endpoint protégé, ajoutez le header Authorization :
curl -X GET http://localhost:3333/api/historique \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."Le token JWT decode se présente comme :
{
"idCompte": 1,
"pseudo": "john_doe",
"iat": 1705753000 // issued at timestamp
}- Par défaut: 24 heures
- Configurable via
JWT_EXPIRATIONen .env
| Code | Message | Signification |
|---|---|---|
| 401 | Token d'authentification manquant | Header Authorization absent |
| 401 | Token invalide | Token corrompu ou incorrect |
| 401 | Token expiré | Token expiré (au-delà de 24h) |
| 403 | Accès refusé | Credentials invalides |
npm install axios react-router-domCréez un fichier src/api/config.js :
import axios from 'axios';
const API_URL = process.env.REACT_APP_API_URL || 'http://localhost:3333';
// Instance axios
const api = axios.create({
baseURL: API_URL,
headers: {
'Content-Type': 'application/json',
},
});
// Intercepteur pour ajouter le token JWT
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// Intercepteur pour gérer les erreurs
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// Token expiré ou invalide
localStorage.removeItem('token');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
export default api;Créez src/api/authService.js :
import api from './config';
const authService = {
register: async (pseudo, password) => {
const response = await api.post('/api/comptes/register', {
pseudo,
password,
});
if (response.data.token) {
localStorage.setItem('token', response.data.token);
localStorage.setItem('userId', response.data.data.idCompte);
localStorage.setItem('pseudo', response.data.data.pseudo);
}
return response.data.data;
},
login: async (pseudo, password) => {
const response = await api.post('/api/comptes/login', {
pseudo,
password,
});
if (response.data.token) {
localStorage.setItem('token', response.data.token);
localStorage.setItem('userId', response.data.data.idCompte);
localStorage.setItem('pseudo', response.data.data.pseudo);
}
return response.data.data;
},
logout: () => {
localStorage.removeItem('token');
localStorage.removeItem('userId');
localStorage.removeItem('pseudo');
},
isAuthenticated: () => {
return !!localStorage.getItem('token');
},
getCurrentUser: () => {
return {
id: localStorage.getItem('userId'),
pseudo: localStorage.getItem('pseudo'),
};
},
};
export default authService;Créez src/api/quizService.js :
import api from './config';
const quizService = {
// Categories
getCategories: async () => {
const response = await api.get('/api/categories');
return response.data.data;
},
getCategoryById: async (id) => {
const response = await api.get(`/api/categories/${id}`);
return response.data.data;
},
// Questions
getQuiz: async (categoryId) => {
const response = await api.get(`/api/questions/quiz/${categoryId}`);
return response.data.data;
},
getRandomQuestion: async (categoryId = null, level = null) => {
let url = '/api/questions/random';
if (categoryId) {
url += `/${categoryId}`;
if (level) {
url += `/${level}`;
}
}
const response = await api.get(url);
return response.data.data;
},
// Historique
getHistory: async (userId) => {
const response = await api.get(`/api/historique/compte/${userId}`);
return response.data.data;
},
getStats: async (userId) => {
const response = await api.get(`/api/historique/stats/${userId}`);
return response.data.data;
},
recordGame: async (userId, categoryId, score, nbQuestions, heureDebut, heureFin) => {
const response = await api.post('/api/historique', {
idCompte: userId,
idCat: categoryId,
score: score,
nbQuestions: nbQuestions,
heureDebut: heureDebut,
heureFin: heureFin,
});
return response.data.data;
},
};
export default quizService;Créez src/hooks/useAuth.js :
import { useState, useContext, createContext } from 'react';
import authService from '../api/authService';
const AuthContext = createContext();
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(authService.getCurrentUser());
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const login = async (pseudo, password) => {
setLoading(true);
setError(null);
try {
const userData = await authService.login(pseudo, password);
setUser(userData);
return userData;
} catch (err) {
setError(err.response?.data?.message || 'Erreur de connexion');
throw err;
} finally {
setLoading(false);
}
};
const register = async (pseudo, password) => {
setLoading(true);
setError(null);
try {
const userData = await authService.register(pseudo, password);
setUser(userData);
return userData;
} catch (err) {
setError(err.response?.data?.message || 'Erreur d\'inscription');
throw err;
} finally {
setLoading(false);
}
};
const logout = () => {
authService.logout();
setUser(null);
};
return (
<AuthContext.Provider
value={{ user, loading, error, login, register, logout }}
>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth doit être utilisé avec AuthProvider');
}
return context;
};import { useState, useEffect } from 'react';
import quizService from '../api/quizService';
import { useAuth } from '../hooks/useAuth';
export default function Quiz({ categoryId }) {
const { user } = useAuth();
const [questions, setQuestions] = useState([]);
const [currentIndex, setCurrentIndex] = useState(0);
const [score, setScore] = useState(0);
const [loading, setLoading] = useState(true);
const [startTime] = useState(new Date());
useEffect(() => {
const loadQuiz = async () => {
try {
const data = await quizService.getQuiz(categoryId);
setQuestions(data);
setLoading(false);
} catch (error) {
console.error('Erreur lors du chargement du quiz:', error);
setLoading(false);
}
};
loadQuiz();
}, [categoryId]);
const handleAnswerClick = async (answerId, isCorrect) => {
// Mettre à jour le score
if (isCorrect) {
setScore(score + 1);
}
// Passer à la question suivante
if (currentIndex < questions.length - 1) {
setCurrentIndex(currentIndex + 1);
} else {
// Quiz terminé - enregistrer la partie
const finalScore = score + (isCorrect ? 1 : 0);
if (user) {
await quizService.recordGame(
user.id,
categoryId,
finalScore,
questions.length,
startTime.toISOString(),
new Date().toISOString()
);
}
alert(`Quiz terminé! Score: ${finalScore}/${questions.length}`);
}
};
if (loading) return <div>Chargement du quiz...</div>;
if (questions.length === 0) return <div>Aucune question</div>;
const currentQuestion = questions[currentIndex];
return (
<div className="quiz-container">
<h2>{currentQuestion.question}</h2>
<p>Question {currentIndex + 1}/{questions.length}</p>
<p>Score: {score}</p>
<div className="answers">
{currentQuestion.reponses.map((answer) => (
<button
key={answer.idRep}
onClick={() => handleAnswerClick(answer.idRep, answer.isBonneRep)}
className="answer-btn"
>
{answer.libelleRep}
</button>
))}
</div>
</div>
);
}import { useState } from 'react';
import { useAuth } from '../hooks/useAuth';
import { useNavigate } from 'react-router-dom';
export default function LoginForm() {
const { login, loading, error } = useAuth();
const navigate = useNavigate();
const [pseudo, setPseudo] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
try {
await login(pseudo, password);
navigate('/quiz');
} catch (err) {
console.error('Erreur de connexion:', err);
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="Pseudo"
value={pseudo}
onChange={(e) => setPseudo(e.target.value)}
required
/>
<input
type="password"
placeholder="Mot de passe"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
<button type="submit" disabled={loading}>
{loading ? 'Connexion...' : 'Se connecter'}
</button>
{error && <p style={{ color: 'red' }}>{error}</p>}
</form>
);
}Créez .env à la racine du projet React :
REACT_APP_API_URL=http://localhost:3333import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { AuthProvider, useAuth } from './hooks/useAuth';
import LoginForm from './components/LoginForm';
import Quiz from './components/Quiz';
function ProtectedRoute({ children }) {
const { user } = useAuth();
return user ? children : <Navigate to="/login" />;
}
export default function App() {
return (
<BrowserRouter>
<AuthProvider>
<Routes>
<Route path="/login" element={<LoginForm />} />
<Route
path="/quiz"
element={
<ProtectedRoute>
<Quiz categoryId={1} />
</ProtectedRoute>
}
/>
<Route path="/" element={<Navigate to="/quiz" />} />
</Routes>
</AuthProvider>
</BrowserRouter>
);
}curl -X POST http://localhost:3333/api/comptes/register \
-H "Content-Type: application/json" \
-d '{
"pseudo": "john_doe",
"password": "MyPassword123!"
}'
# Réponse:
# {
# "success": true,
# "data": {
# "id": 1,
# "pseudo": "john_doe",
# "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
# }
# }
# Sauvegarder le token
TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."curl -X GET http://localhost:3333/api/categoriescurl -X GET http://localhost:3333/api/questions/quiz/1curl -X POST http://localhost:3333/api/historique \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"idCompte": 1,
"idCat": 1,
"score": 8,
"nbQuestions": 10,
"heureDebut": "2024-01-20T14:00:00Z",
"heureFin": "2024-01-20T14:07:00Z"
}'curl -X GET http://localhost:3333/api/historique/stats/1 \
-H "Authorization: Bearer $TOKEN"# Démarrer les services (PostgreSQL + API)
docker-compose up -d
# Arrêter les services
docker-compose down
# Voir les logs
docker-compose logs -f api
# Reconstruire après des changements de code
docker-compose up -d --build
# Accéder à la DB en CLI
docker exec -it culturequiz_db psql -U culturequiz_user -d culturequiz┌─────────────────────────────────────────┐
│ docker-compose.yml │
├─────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ API │───│ PostgreSQL │ │
│ │ :3333 │ │ :5432 │ │
│ └──────────────┘ └──────────────┘ │
│ │
│ Network: culturequiz_network │
└─────────────────────────────────────────┘
# Status des conteneurs
docker-compose ps
# Logs du service API
docker-compose logs api
# Logs du service DB
docker-compose logs postgresL'API inclut un système de backup automatique hebdomadaire pour PostgreSQL.
- Fréquence: Chaque dimanche à 2h du matin
- Rétention: 30 jours
- Format: SQL compressé (.sql.gz)
- Stockage: Volume Docker
backup_data
# Forcer un backup manuel
docker exec culturequiz_backup /usr/local/bin/backup.sh
# Voir les logs de backup
docker exec culturequiz_backup cat /var/log/backup.log
# Lister les backups disponibles
docker exec culturequiz_backup ls -lh /backups/
# Restaurer un backup
docker exec culturequiz_backup sh -c "gunzip -c /backups/culturequiz_backup_YYYYMMDD_HHMMSS.sql.gz" | docker exec -i culturequiz_db psql -U culturequiz_user -d culturequiz
# Exporter un backup vers votre machine
docker cp culturequiz_backup:/backups/culturequiz_backup_YYYYMMDD_HHMMSS.sql.gz ./📖 Documentation complète: Consultez backup/README.md
Solution:
# Vérifier que le serveur est lancé
npm run dev
# Ou avec Docker
docker-compose up -d apiVérifier les variables d'environnement:
# Vérifier le fichier .env
cat .env
# Vérifier la connectivité DB
psql -h localhost -U culturequiz_user -d culturequizSolutions:
- Le token a expiré (> 24h) → Se reconnecter
- Le format du header n'est pas correct → Utiliser
Authorization: Bearer <token> - La clé JWT_SECRET ne correspond pas → Vérifier .env
Solution dans .env:
CORS_ORIGIN=http://localhost:3000Puis redémarrer l'API:
npm run dev# Trouver le processus utilisant le port
lsof -i :3333 # macOS/Linux
netstat -ano | findstr :3333 # Windows
# Tuer le processus (Windows)
taskkill /PID <PID> /F
# Ou changer le PORT dans .env
PORT=3334Vérifier:
- Le domaine CORS dans le fichier
.envde l'API - Le stockage localStorage n'est pas bloqué
- L'intercepteur axios utilise bien
localStorage.getItem('token')
┌──────────────┐ ┌──────────────────────┐
│ comptes │ │ historiqueScore │
├──────────────┤ ├──────────────────────┤
│ id (PK) │◄───────│ idHistorique (PK) │
│ pseudo │ │ idCompte (FK) ────┐ │
│ password │ │ idCat (FK) │ │
│ dateCreation │ │ score │ │
│ dateModif │ │ tempsJeu │ │
└──────────────┘ │ datePartie │ │
└──────────────────┘ │
│ │
┌─────────┴───────────────┘
│
┌──────────▼──────────┐
│ categories │
├────────────────────┤
│ idCat (PK) │
│ libelleCategorie │
│ diminutif │
└────────────────────┘
│
┌──────────▼──────────┐
│ questions │
├────────────────────┤
│ idQue (PK) │
│ libelleQue │
│ niveau │
│ idCat (FK) ────────┘
└────────────────────┘
│
┌──────────▼──────────┐
│ reponses │
├────────────────────┤
│ idRep (PK) │
│ libelleRep │
│ isBonneRep │
│ idQue (FK) ────────┘
└────────────────────┘
- Compte sur une plateforme cloud (Heroku, Railway, AWS, Azure, etc.)
- Base de données PostgreSQL en cloud
- Variables d'environnement configurées
# 1. Installer Railway CLI
npm install -g @railway/cli
# 2. Authentification
railway login
# 3. Initialiser le projet
railway init
# 4. Créer une base de données PostgreSQL
railway add postgres
# 5. Déployer
railway up
# 6. Vérifier les logs
railway logsNODE_ENV=production
PORT=3333
JWT_SECRET=une_cle_secrete_vraiment_très_longue_et_aléatoire
DB_HOST=<host_db_cloud>
DB_PORT=5432
DB_USER=<user_db>
DB_PASSWORD=<password_db>
DB_NAME=culturequiz
CORS_ORIGIN=https://votredomaine.com- Documentation Express: https://expressjs.com/
- Documentation PostgreSQL: https://www.postgresql.org/docs/
- JWT Handbook: https://auth0.com/resources/ebooks/jwt-handbook
- React Documentation: https://react.dev/
Ce projet est sous licence ISC.
Dernière mise à jour: Janvier 2026
Version API: 1.0.0