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: {
indent: ["error", "tab"],
"linebreak-style": ["error", "unix"],
"no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
quotes: ["error", "double"],
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
CREATE TABLE dinosaur (
id SERIAL PRIMARY KEY,
name VARCHAR(256) NOT NULL,
name VARCHAR(256) NOT NULL UNIQUE,
description VARCHAR(4096) NOT NULL,
period VARCHAR(32) 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)
VALUES (
'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"
}
}