Skip to content
Open
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
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
DATABASE_URL =
PORT =
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
.env
dist
144 changes: 78 additions & 66 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,66 +1,78 @@

# Project: Bookstore API
## Overview:
Build a RESTful API for a bookstore application using Node.js, Express, and TypeScript. The API should manage books, authors, and categories. Each book has a title, author, category, publication year, and ISBN.

## Requirements:
### Setup:

- Initialize a new Node.js project using npm or yarn.
- Use TypeScript for your project.
#### Express Setup:

Set up an Express application with appropriate middleware.
Include middleware for JSON parsing and logging.

#### Routes:
Create routes for the following CRUD operations:

##### Books:
- Create a new book.
- Get a list of all books.
- Get details of a specific book.
- Update the details of a book.
- Delete a book.

##### Authors:
- Create a new author.
- Get a list of all authors.
- Get details of a specific author.
- Update the details of an author.
- Delete an author.

##### Categories:
- Create a new category.
- Get a list of all categories.
- Get details of a specific category.
- Update the details of a category.
- Delete a category.

#### Data Storage:
- Use an in-memory array or a simple database (e.g., MongoDB or MySQL) to store books, authors, and categories.
- Implement appropriate relationships between books, authors, and categories.

#### Validation:
- Validate the input data for creating and updating books, authors, and categories.
- Include appropriate error handling and return meaningful error messages.

#### Testing:
Write unit tests for at least two routes using a testing framework of your choice (Jest, Mocha, etc.).

#### Documentation:
- Provide clear documentation on how to run your application and tests.
- Include a brief overview of the project structure and any important design decisions.
- Use Postman to document your endpoints

#### Bonus Points:
- Implement sorting and filtering options for the list of books, authors, and categories.
- Add pagination for the list endpoints.
- Include user authentication middleware.

#### Submission Guidelines:
- Fork this repository and commit your code.
- Include a README.md file with instructions on how to run the application and tests.
- Create a pull request with your completed assessment.


# Kalu Chibuikem Victor - BookStore Api

## Overview

This is a backend built with express.js, node and typescript.

Routes: A common way of structuring a backend would be to have separate folders for `routes`,`services` and `controllers`. I didn't go this route 😅 as I found i worked faster on this project with the databse logic in the controller.

Middlewares: I have a middleware folder, for logging and maybe where I could have added authentication.I decided to write the logging logic to demonstrate my control of middlewares in an express application.

Memory: It is connected to a Postgres Database using drizzle as an ORM. I went with drizzle as it is lighter and has an sql first design pattern. The postgres database is hosted locally to test the application please provide your own postgres Database.

Testing: I am using vitest. I chose this cause when i tried setting up jest I had module errors with typescript i just had to stick with what i know at that moment. The unit tests I write are for specific functionalities of the application, such as: Testing the pagination function calculation. I didn't know how to test a route, but, It is something I am willing to learn.

## Getting Started

To begin run

```bash
yarn install
```

First: Create a .env file with these values filled

```
DATABASE_URL = ''
PORT = ''
```

Second:

```bash
yarn db:generate
```

This is to generate the types needed for the typescript compiler .

Third:

```bash
yarn db:push
```

This is upload the schema of the application to your database.

Finally:

```bash
yarn dev
```

### Scripts

Here are scripts that are used to run this application.

```json
"scripts": {
"dev": "nodemon index",
"start": "node dist/index.js",
"build": "tsc",
"test": "vitest",
"db:generate": "drizzle-kit generate:pg --config=drizzle.config.ts",
"db:check": "drizzle-kit check:pg --config=drizzle.config.ts",
"db:migrate": "ts-node ./migrate.ts",
"db:studio": "drizzle-kit studio --config=drizzle.config.ts",
"db:push": "drizzle-kit push:pg --config=drizzle.config.ts"
}
```

## Tests

```bash
yarn test
```

## Endpoints

<img width="400" src="/endpoints_img.png" alt="Endpoints">
18 changes: 18 additions & 0 deletions drizzle.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { Config } from "drizzle-kit";
import * as dotenv from "dotenv";

dotenv.config();

if (!process.env.DATABASE_URL) {
throw new Error("DATABASE_URL is missing");
}

export default {
schema: "./src/database/schema.ts",
out: "./migrations",
driver: "pg",
breakpoints: true,
dbCredentials: {
connectionString: process.env.DATABASE_URL,
},
} satisfies Config;
Binary file added endpoints_img.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import app from "./server";
import dotenv from "dotenv";
dotenv.config();

const PORT = process.env.PORT || 8080;

app.listen(PORT, () => {
console.log(`Server is running at http://localhost:${PORT}`);
});
38 changes: 38 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"name": "wesher-intern",
"version": "1.0.0",
"main": "index.ts",
"license": "MIT",
"scripts": {
"dev": "nodemon index",
"start": "node dist/index.js",
"build": "tsc",
"test": "vitest",
"db:generate": "drizzle-kit generate:pg --config=drizzle.config.ts",
"db:check": "drizzle-kit check:pg --config=drizzle.config.ts",
"db:migrate": "ts-node ./migrate.ts",
"db:studio": "drizzle-kit studio --config=drizzle.config.ts",
"db:push": "drizzle-kit push:pg --config=drizzle.config.ts"
},
"devDependencies": {
"@types/cookie-parser": "^1.4.6",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/node": "^20.9.4",
"drizzle-kit": "^0.20.14",
"nodemon": "^3.0.1",
"ts-node": "^10.9.1",
"typescript": "^5.3.2",
"vitest": "^1.3.1"
},
"dependencies": {
"@paralleldrive/cuid2": "^2.2.2",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"drizzle-orm": "^0.29.4",
"express": "^4.18.2",
"postgres": "^3.4.3",
"zod": "^3.22.4"
}
}
26 changes: 26 additions & 0 deletions server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import cookieParser from "cookie-parser";
import dotenv from "dotenv";
import express from "express";
import { logger } from "./src/middlewares/logger";
import authorRoute from "./src/modules/routes/author";
import bookRoute from "./src/modules/routes/book";
import categoryRoute from "./src/modules/routes/category";

dotenv.config();
const app = express();

//Middlewares
app.use(express.json());
app.use(cookieParser());
app.use(logger);

//Routes
app.use("/api/v1/books", bookRoute);
app.use("/api/v1/authors", authorRoute);
app.use("/api/v1/categories", categoryRoute);

app.use("*", (req, res) => {
res.status(404).json({ message: "This Route Not Found" });
});

export default app;
35 changes: 35 additions & 0 deletions src/api_schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import z from "zod";

export const bookSchema = z.object({
title: z.string().min(2, "The title has a minimum length of 2"),
author: z.string(),
category: z.string(),
publicationYear: z.number(),
ISBN: z.string(),
});

export const bookSchemaUpdate = z.object({
title: z.string().min(2, "The title has a minimum length of 2").optional(),
author: z.string().optional(),
category: z.string().optional(),
publicationYear: z.number().optional(),
ISBN: z.string().optional(),
});

export const authorSchema = z.object({
name: z.string(),
email: z.string().email(),
});

export const authorSchemaUpdate = z.object({
name: z.string().optional(),
email: z.string().email().optional(),
});

export const categorySchema = z.object({
name: z.string(),
});

export const categorySchemaUpdate = z.object({
name: z.string(),
});
12 changes: 12 additions & 0 deletions src/database/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import * as dotenv from "dotenv";
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import * as schema from "./schema";

dotenv.config();

const connectionString = process.env.DATABASE_URL || "";

const connection = postgres(connectionString);

export const db = drizzle(connection, { logger: true, schema });
34 changes: 34 additions & 0 deletions src/database/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { createId } from "@paralleldrive/cuid2";
import { integer, pgTable, text, timestamp } from "drizzle-orm/pg-core";

export const author = pgTable("author", {
id: text("id").$defaultFn(createId).notNull().primaryKey(),
name: text("name").notNull(),
email: text("email").notNull(),
// password: text("password")
// image: text("image"),
created_at: timestamp("created_at").defaultNow(),
updated_at: timestamp("updated_at").defaultNow(),
});

export const category = pgTable("category", {
id: text("id").$defaultFn(createId).notNull().primaryKey(),
name: text("category").notNull(),
created_at: timestamp("created_at").defaultNow(),
updated_at: timestamp("updated_at").defaultNow(),
});

export const book = pgTable("book", {
id: text("id").$defaultFn(createId).notNull().primaryKey(),
title: text("title").notNull().unique(),
author: text("author")
.notNull()
.references(() => author.name),
category: text("category")
.notNull()
.references(() => category.name),
isbn: text("isbn").notNull(),
publicationYear: integer("publication_year").notNull(),
created_at: timestamp("created_at").defaultNow(),
updated_at: timestamp("updated_at").defaultNow(),
});
27 changes: 27 additions & 0 deletions src/middlewares/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Request, Response } from "express";

const reqTimeStamps: any = {};

export const logger = (req: Request, res: Response, next: any) => {
console.log(`Request URL: ${req.originalUrl}`);
console.log(`Request Type: ${req.method}`);
const reqType = `${req.method} ${req.path}`;
const currentTime = new Date().getTime();
const value = reqTimeStamps[reqType];
const timeSinceLatRequest = value ? currentTime - value : 0;

reqTimeStamps[reqType] = currentTime;
const original = res.send;

//@ts-ignore
res.send = function (body) {
const responseTime = Date.now() - currentTime;

console.log(
`Status: ${res.statusCode} |\nResponse Time: ${responseTime}ms |\nTime since last Request: ${reqType} = ${timeSinceLatRequest}ms`
);

original.call(this, body);
};
next();
};
Loading