Build layered dinosaur database API

This commit is contained in:
Tomas Krejci 2026-06-15 19:37:08 +02:00
parent 8e5912fcf9
commit 3739142e6b
16 changed files with 2009 additions and 1823 deletions

View File

@ -1,5 +0,0 @@
{
"endOfLine": "lf",
"useTabs": true,
"printWidth": 120
}

View File

@ -0,0 +1,6 @@
PGHOST="localhost"
PGPORT="5432"
PGDATABASE="rg_academy_dev"
PGUSER="rg_academy"
PGPASSWORD="rg_academy"
PORT="3000"

View File

@ -0,0 +1,5 @@
{
"endOfLine": "lf",
"useTabs": true,
"printWidth": 120
}

View File

@ -0,0 +1,11 @@
### List all dinosaurs
GET http://localhost:3000/dinosaurs
### Get one dinosaur
GET http://localhost:3000/dinosaurs/1
### Invalid dinosaur ID
GET http://localhost:3000/dinosaurs/not-a-number
### Missing dinosaur
GET http://localhost:3000/dinosaurs/999999

View File

@ -15,6 +15,7 @@ export default defineConfig([
rules: { rules: {
indent: ["error", "tab"], indent: ["error", "tab"],
"linebreak-style": ["error", "unix"], "linebreak-style": ["error", "unix"],
"no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
quotes: ["error", "double"], quotes: ["error", "double"],
semi: ["error", "always"], semi: ["error", "always"],
}, },

View File

@ -0,0 +1,39 @@
import "dotenv/config";
import express from "express";
import { Client } from "pg";
// Legacy single-file implementation kept for comparison with the layered src/ structure.
const client = new Client({
host: process.env.PGHOST,
port: Number(process.env.PGPORT),
database: process.env.PGDATABASE,
user: process.env.PGUSER,
password: process.env.PGPASSWORD,
});
const app = express();
app.get("/dinosaurs", async (request, response) => {
const result = await client.query("SELECT * FROM dinosaur ORDER BY id");
const dinosaurs = result.rows.map((row) => ({
id: row.id,
name: row.name,
description: row.description,
period: row.period,
wikipediaAddress: row.wikipedia_address,
}));
response.json(dinosaurs);
});
await client.connect();
console.log("Connected to database");
app.listen(3000, () => {
console.log("Server running on port 3000");
});
process.on("SIGINT", async () => {
console.log("Shutting down...");
await client.end();
process.exit(0);
});

1796
lecture_5/dinosaurs/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,23 @@
{
"name": "dinosaurs",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "node src/index.js",
"lint": "eslint .",
"format": "prettier --write .",
"check": "npm run lint && prettier --check ."
},
"dependencies": {
"dotenv": "^17.4.2",
"express": "^5.1.0",
"pg": "^8.11.3"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@eslint/json": "^1.2.0",
"eslint": "^10.0.0",
"globals": "^17.4.0",
"prettier": "^3.0.0"
}
}

View File

@ -1,13 +1,13 @@
-- Dinosaur table -- Dinosaur table
CREATE TABLE dinosaur ( CREATE TABLE dinosaur (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
name VARCHAR(256) NOT NULL, name VARCHAR(256) NOT NULL UNIQUE,
description VARCHAR(4096) NOT NULL, description VARCHAR(4096) NOT NULL,
period VARCHAR(32) NOT NULL, period VARCHAR(32) NOT NULL,
wikipedia_address VARCHAR(4096) NOT NULL wikipedia_address VARCHAR(4096) NOT NULL
); );
-- Insert sample data into dinosaur table -- Seed records provide predictable data for the API exercises.
INSERT INTO dinosaur (name, description, period, wikipedia_address) INSERT INTO dinosaur (name, description, period, wikipedia_address)
VALUES ( VALUES (
'Tyrannosaurus', 'Tyrannosaurus',

View File

@ -0,0 +1,9 @@
import express from "express";
import { dinosaurRouter } from "./routes/dinosaurRoutes.js";
const app = express();
app.use(express.json());
app.use(dinosaurRouter);
export { app };

View File

@ -0,0 +1,5 @@
import "dotenv/config";
import { Client } from "pg";
// The pg client reads the standard PGHOST, PGPORT, PGDATABASE, PGUSER, and PGPASSWORD variables.
export const client = new Client();

View File

@ -0,0 +1,23 @@
import { client } from "./db/client.js";
import { app } from "./app.js";
const port = Number(process.env.PORT ?? 3000);
try {
await client.connect();
console.log("Connected to database");
app.listen(port, () => {
console.log(`Server running on http://localhost:${port}`);
});
} catch (error) {
console.error("Failed to start the server:", error.message);
process.exitCode = 1;
}
// Close the database connection when the process is stopped from the terminal.
process.on("SIGINT", async () => {
console.log("Shutting down...");
await client.end();
process.exit(0);
});

View File

@ -0,0 +1,58 @@
import { client } from "../db/client.js";
/**
* @typedef {Object} DinosaurWithoutId
* @property {string} name
* @property {string} description
* @property {string} period
* @property {string} wikipediaAddress
*/
/**
* @typedef {DinosaurWithoutId} Dinosaur
* @property {number} id
*/
/**
* Converts a database row representing a dinosaur into a `Dinosaur` object.
*
* @param {Object} row
* @returns {Dinosaur}
*/
const mapRowToDinosaurDto = (row) => {
return {
id: row.id,
name: row.name,
description: row.description,
period: row.period,
wikipediaAddress: row.wikipedia_address,
};
};
export const dinosaurRepository = {
/**
* Obtains all dinosaurs, ordered by ID.
*
* @returns {Promise<Dinosaur[]>}
*/
async listDinosaurs() {
const result = await client.query("SELECT * FROM dinosaur ORDER BY id");
return result.rows.map(mapRowToDinosaurDto);
},
/**
* Obtains one dinosaur by its ID.
*
* @param {number} id
* @returns {Promise<Dinosaur | null>}
*/
async getDinosaurById(id) {
const result = await client.query("SELECT * FROM dinosaur WHERE id = $1", [id]);
if (result.rowCount === 0) {
return null;
}
return mapRowToDinosaurDto(result.rows[0]);
},
};

View File

@ -0,0 +1,31 @@
import express from "express";
import { dinosaurRepository } from "../repositories/dinosaurRepository.js";
export const dinosaurRouter = express.Router();
// Return all dinosaurs through the repository abstraction.
dinosaurRouter.get("/dinosaurs", async (_request, response) => {
const dinosaurs = await dinosaurRepository.listDinosaurs();
response.json(dinosaurs);
});
// Return one dinosaur identified by a positive integer ID.
dinosaurRouter.get("/dinosaurs/:id", async (request, response) => {
const id = Number(request.params.id);
if (!Number.isInteger(id) || id <= 0) {
return response.status(400).json({
message: "Dinosaur ID must be a positive integer",
});
}
const dinosaur = await dinosaurRepository.getDinosaurById(id);
if (dinosaur === null) {
return response.status(404).json({
message: "Dinosaur not found",
});
}
response.json(dinosaur);
});

File diff suppressed because it is too large Load Diff

View File

@ -1,20 +0,0 @@
{
"name": "dinosaurs",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"dotenv": "^17.4.2",
"express": "^5.1.0",
"pg": "^8.11.3"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@eslint/json": "^1.2.0",
"eslint": "^10.0.0",
"globals": "^17.4.0",
"prettier": "^3.0.0"
}
}