Implementing and Testing APIs
Emanuel Sanchez
Software Engineer | Test Automation, Linux, CI/CD, Cloud Computing, ML
Today, we're putting our knowledge into action by implementing and testing the Translations API for our language learning app. This API will form the core of our application, connecting words across different languages. Let's see how our previous experiences shape this implementation. ????
Database Structure Recap
Before we dive into the API implementation, let's quickly recap our database structure:
CREATE TABLE public.words (
id integer NOT NULL,
word character varying(255) NOT NULL
);
CREATE TABLE public.languages (
id integer NOT NULL,
name character varying(255) NOT NULL,
code character varying(5)
);
CREATE TABLE public.translations (
id integer NOT NULL,
word_id integer NOT NULL,
language_id integer NOT NULL,
translation character varying(255) NOT NULL
);
This structure allows us to store words, languages, and their translations efficiently. The translations table acts as a join table, connecting words to their translations in various languages.
API Implementation
Now, let's implement our Translations API:
const express = require('express');
const { Pool } = require('pg');
const app = express();
app.use(express.json());
const pool = new Pool(/* connection details */);
// GET all translations
app.get('/translations', async (req, res) => {
try {
const result = await pool.query('SELECT * FROM translations');
res.json(result.rows);
} catch (err) {
console.error(err);
res.status(500).json({ message: 'Internal Server Error' });
}
});
// GET a specific translation
app.get('/translations/:id', async (req, res) => {
const id = parseInt(req.params.id);
if (isNaN(id)) {
return res.status(400).json({ message: 'Invalid translation ID' });
}
try {
const result = await pool.query('SELECT * FROM translations WHERE id = $1', [id]);
if (result.rows.length === 0) {
return res.status(404).json({ message: 'Translation not found' });
}
res.json(result.rows[0]);
} catch (err) {
console.error(err);
res.status(500).json({ message: 'Internal Server Error' });
}
});
// POST a new translation
app.post('/translations', async (req, res) => {
const { wordId, languageId, translation } = req.body;
if (!wordId || !languageId || !translation) {
return res.status(400).json({ message: 'WordId, languageId, and translation are required' });
}
try {
const result = await pool.query(
'INSERT INTO translations (word_id, language_id, translation) VALUES ($1, $2, $3) RETURNING *',
[wordId, languageId, translation]
);
res.status(201).json(result.rows[0]);
} catch (err) {
console.error(err);
if (err.constraint === 'translations_word_id_fkey' || err.constraint === 'translations_language_id_fkey') {
res.status(400).json({ message: 'Invalid wordId or languageId' });
} else {
res.status(500).json({ message: 'Internal Server Error' });
}
}
});
// PUT (update) a translation
app.put('/translations/:id', async (req, res) => {
const id = parseInt(req.params.id);
const { translation } = req.body;
if (isNaN(id)) {
return res.status(400).json({ message: 'Invalid translation ID' });
}
if (!translation || translation.trim() === '' || translation.length > 255) {
return res.status(400).json({ message: 'Invalid translation: must be non-empty and no longer than 255 characters' });
}
try {
const result = await pool.query(
'UPDATE translations SET translation = $1 WHERE id = $2 RETURNING *',
[translation, id]
);
if (result.rows.length === 0) {
return res.status(404).json({ message: 'Translation not found' });
}
res.json(result.rows[0]);
} catch (err) {
console.error(err);
res.status(500).json({ message: 'Internal Server Error' });
}
});
// DELETE a translation
app.delete('/translations/:id', async (req, res) => {
const id = parseInt(req.params.id);
if (isNaN(id)) {
return res.status(400).json({ message: 'Invalid translation ID' });
}
try {
const result = await pool.query('DELETE FROM translations WHERE id = $1 RETURNING *', [id]);
if (result.rows.length === 0) {
return res.status(404).json({ message: 'Translation not found' });
}
res.json({ message: 'Translation deleted successfully' });
} catch (err) {
console.error(err);
res.status(500).json({ message: 'Internal Server Error' });
}
});
// GET translations for a specific word
app.get('/words/:wordId/translations', async (req, res) => {
const wordId = parseInt(req.params.wordId);
if (isNaN(wordId)) {
return res.status(400).json({ message: 'Invalid word ID' });
}
try {
const result = await pool.query(
'SELECT t.*, l.name as language_name FROM translations t JOIN languages l ON t.language_id = l.id WHERE t.word_id = $1',
[wordId]
);
res.json(result.rows);
} catch (err) {
console.error(err);
res.status(500).json({ message: 'Internal Server Error' });
}
});
Testing our Translations API
Here's how we can test our API using Jest and Supertest:
领英推荐
const request = require('supertest');
const app = require('../app');
const { Pool } = require('pg');
jest.mock('pg', () => {
const mPool = {
query: jest.fn(),
};
return { Pool: jest.fn(() => mPool) };
});
const pool = new Pool();
beforeEach(() => {
jest.clearAllMocks();
});
describe('Translations API', () => {
describe('GET /translations', () => {
it('should return all translations', async () => {
const mockTranslations = [
{ id: 1, word_id: 1, language_id: 1, translation: 'Hola' },
{ id: 2, word_id: 1, language_id: 2, translation: 'Bonjour' }
];
pool.query.mockResolvedValueOnce({ rows: mockTranslations });
const res = await request(app).get('/translations');
expect(res.statusCode).toBe(200);
expect(res.body).toEqual(mockTranslations);
});
});
describe('POST /translations', () => {
it('should create a new translation', async () => {
const newTranslation = { wordId: 1, languageId: 1, translation: 'Hola' };
pool.query.mockResolvedValueOnce({ rows: [{ id: 1, ...newTranslation }] });
const res = await request(app)
.post('/translations')
.send(newTranslation);
expect(res.statusCode).toBe(201);
expect(res.body).toEqual({ id: 1, ...newTranslation });
});
it('should return 400 if required fields are missing', async () => {
const res = await request(app)
.post('/translations')
.send({ wordId: 1 });
expect(res.statusCode).toBe(400);
expect(res.body).toEqual({ message: 'WordId, languageId, and translation are required' });
});
});
describe('PUT /translations/:id', () => {
it('should update an existing translation', async () => {
const updatedTranslation = { translation: 'Hola updated' };
pool.query.mockResolvedValueOnce({ rows: [{ id: 1, ...updatedTranslation }] });
const res = await request(app)
.put('/translations/1')
.send(updatedTranslation);
expect(res.statusCode).toBe(200);
expect(res.body).toEqual({ id: 1, ...updatedTranslation });
});
it('should return 400 if translation is empty', async () => {
const res = await request(app)
.put('/translations/1')
.send({ translation: '' });
expect(res.statusCode).toBe(400);
expect(res.body).toEqual({ message: 'Invalid translation: must be non-empty and no longer than 255 characters' });
});
it('should return 400 if translation is too long', async () => {
const longTranslation = 'a'.repeat(256);
const res = await request(app)
.put('/translations/1')
.send({ translation: longTranslation });
expect(res.statusCode).toBe(400);
expect(res.body).toEqual({ message: 'Invalid translation: must be non-empty and no longer than 255 characters' });
});
});
describe('GET /words/:wordId/translations', () => {
it('should return translations for a specific word', async () => {
const mockTranslations = [
{ id: 1, word_id: 1, language_id: 1, translation: 'Hola', language_name: 'Spanish' },
{ id: 2, word_id: 1, language_id: 2, translation: 'Bonjour', language_name: 'French' }
];
pool.query.mockResolvedValueOnce({ rows: mockTranslations });
const res = await request(app).get('/words/1/translations');
expect(res.statusCode).toBe(200);
expect(res.body).toEqual(mockTranslations);
});
it('should return 400 for invalid word ID', async () => {
const res = await request(app).get('/words/invalid/translations');
expect(res.statusCode).toBe(400);
expect(res.body).toEqual({ message: 'Invalid word ID' });
});
});
});
Key Takeaways:
1. Database Structure: Our schema efficiently represents the relationships between words, languages, and translations.
2. Consistency: We've maintained a consistent structure across all our API endpoints.
3. Error Handling: We've implemented robust error handling, including foreign key constraint violations and input validation.
4. Relationships: The '/words/:wordId/translations' endpoint demonstrates handling related data.
5. Testing: Our tests cover both happy paths and error scenarios, ensuring API reliability.
Building this Translations API has reinforced the importance of consistent design, thorough testing, and careful consideration of data relationships. As we continue to develop our language learning app, these principles will guide us towards a robust and scalable solution.
#webdevelopment #apitesting #nodejs #learninginpublic