Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion NOTES.md
Original file line number Diff line number Diff line change
@@ -1 +1,23 @@
Please add any additional notes here…
Please add any additional notes here…


high level slices:
run server
load and inspect json data
expose two endpoints
return appropriate Json responses

slices of slices:
- Save the url as consts here in the server file?
- add a function(s) to get the data (could write just one that takes a url as an arg, and then I can call that in more specific functions later?)
These could then be called in the GET.
- save the correct props/target the correct props, to a var.


Decisions:
seperate concrns with different functions, but use a resusable function for fetching- though caching would be important here in the real world.

The mapping of the data is something I would have liked to extract into mapping functions that could be imported to make the code cleaner, and increase resusability if required.

Should have included pagination to gaurd against larger payloads.

37 changes: 37 additions & 0 deletions src/__tests__/server.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import request from 'supertest'
import { app } from '../server'


test('returns all cards with correct structure', async () => {
const response = await request(app).get('/cards')

expect(response.status).toBe(200)
expect(response.body).toEqual(
expect.arrayContaining([
expect.objectContaining({ title: 'card 1 title', url: '/cards/card001' })
]))
})
test('returns matching card title', async () => {
const response = await request(app).get('/cards/card001')

Expand All @@ -9,3 +19,30 @@ test('returns matching card title', async () => {
title: 'card 1 title',
}))
})
test('returns 404 if card not found', async () => {
const response = await request(app).get('/cards/invalidCard')

expect(response.status).toBe(404)
expect(response.body).toEqual(expect.objectContaining({
"error": "Card not found"
}))
})

test('returns matching card size', async () => {
const response = await request(app).get('/cards/card001/sm')

expect(response.status).toBe(200)
expect(response.body).toEqual(expect.objectContaining({
title: 'card 1 title',
price: "£1.60",
imageUrl: '/front-cover-portrait-1.jpg'
}))
})
test('returns 404 if card size not found', async () => {
const response = await request(app).get('/cards/card001/invalidSize')

expect(response.status).toBe(404)
expect(response.body).toEqual(expect.objectContaining({
"error": "Card size not found"
}))
})
78 changes: 74 additions & 4 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,80 @@ export const app = express()

app.set('json spaces', 2);

app.get('/cards', async () => {
// respond with a list of cards
const cardsUrl = "https://moonpig.github.io/tech-test-node-backend/cards.json"
const sizesUrl = "https://moonpig.github.io/tech-test-node-backend/sizes.json"
const templatesUrl = "https://moonpig.github.io/tech-test-node-backend/templates.json"

async function fetchJson(url: string) {
const res = await fetch(url);
if (!res.ok) throw new Error(`Failed to fetch ${url}`);
return res.json();
}

async function getAllCards() {
return fetchJson(cardsUrl);
}
async function getTemplates() {
return fetchJson(templatesUrl);
}
async function getSizes() {
return fetchJson(sizesUrl);
}


app.get('/cards', async (req, res) => {
try{
const cards = await getAllCards();
const templates = await getTemplates();

const result = cards.map((card) => {
const firstPage = card.pages[0];
const firstPageTemplate = templates.find(t => t.id === firstPage.templateId);

return {
title: card.title,
url: `/cards/${card.id}`,
imageUrl: firstPageTemplate ? firstPageTemplate.imageUrl : "No image available",
};
})
res.json(result);
} catch (err) {
console.error(err);
res.status(500).json({ error: "Failed to fetch cards" });
}
})

app.get('/cards/:cardId/:sizeId?', () => {
// respond with card by id
app.get('/cards/:cardId/:sizeId?', async (req, res) => {
try{
const { cardId, sizeId } = req.params;
const cards = await getAllCards();
const templates = await getTemplates();
const sizes = await getSizes();

const card = cards.find(card => card.id === cardId);
if (!card) {
return res.status(404).json({ error: "Card not found" });
}
const selectedSize = sizeId
? sizes.find(s => s.id === sizeId)
: undefined;

if (sizeId && !selectedSize) {
return res.status(404).json({ error: "Card size not found" });
}
const pricePence = selectedSize ? card.basePrice * selectedSize.priceMultiplier : card.basePrice;
const price = `£${(pricePence / 100).toFixed(2)}`;

const template = templates.find(t => t.id === card.pages[0].templateId);

res.json({
title: card.title,
price,
imageUrl: template ? template.imageUrl : "No image available",
});

} catch (err) {
console.error(err);
res.status(500).json({ error: "Failed to fetch cards" });
}
})