Quotes API

Intro

We will create a quotes API for fetching random quotes from a database of quotes. We can use the API in the Random Quote Machine.

NOTE: This step is not required for the certification project. We could alternatively use an API provider like API Ninjas
NOTE: This step requires a working MongoDB installation. We can use the free tier of MongoDB Atlas.

Check out the code at https://github.com/yasakdogra/quotesapi

Step 1: Quotes Database

We will create a small project with a single typescript file to upload the quotes to a MongoDB instance. We will only need to run this script once.

NOTE: We are not adding any data sanity checks or error handlers. If there are any errors or the upload is interrupted, we can always delete the quotes manually and run the script again.

We will use Bun to create and run the script as well as manage dependencies for it. NPM and a few other have very similar procedure.

The quotes are in a json file called quotes.json. This file will live in the root directory of our project along with our typescript file.

Start by initializing bun by running bun init. Hit enter to accept default package name and entry point.

% bun init

Install dependencies

% bun install dotenv mongoose

Add the MongoDB connection url to .env file in the project root replacing <user>with username, <pass> with password and <instance> with the instance id. The connection string can be generated from MongoDB Atlas dashboard. We will use this connection string in the typescript file using dotenv package.

MONGO_URI=mongodb+srv://<user>:<pass>@<instance>.mongodb.net/quotes?retryWrites=true&w=majority

In the index.ts file, add import statements

import 'dotenv/config'
import fs from 'fs/promises'
import mongoose from "mongoose"

Add a type for quotes that matches the quote structure in json file

type quoteEntry = {
    quote: String,
    author: String
}

Create mongoose schema and model for quotes

const quoteSchema = new mongoose.Schema({
    quote: String,
    author: String
});

const Quote = mongoose.model('Quote', quoteSchema);

Add function that takes a list of quotes and saves them to MongoDB

const addToDB = (quotesList: [quoteEntry]) => {
    Quote.create(quotesList)
}

Connect to MongoDB using the connection string created in the .env file

if(process.env.MONGO_URI) {
    await mongoose.connect(process.env.MONGO_URI)
}
else {
    console.log('MONGO_URI missing')
    process.exit()
}

Read the quotes file and use the function created above to save the quotes

fs.readFile('quotes.json', 'utf8')
.then((data: any) => addToDB(JSON.parse(data)))
.catch((err: any) => console.log(err))

You can also await the promises and add some console.log messages to check what part the script is working on.

Finally run the script using bun run index.ts or bun run dev.

The script running time will depend on how many quotes are uploaded. I was able to do a few thousand qutoes in about a minute.

Step 2: Quotes API

The setup for this part will be very similar to the previous step where we uploaded the quotes to MongoDB. The big difference is that the upload script runs once and exits whereas this API will be continuously running and serving requests for fetching quotes.

We will use Hono web framework for handing HTTP GET requests and returning quotes in JSON format.

Use create-hono to setup a project with bun template to add necessary files.

% bun create hono api

Next we will change to the project directory and install dotenv and mongoose

% bun install dotenv mongoose

Refer to previous step to create .env file and add MONGO_URIto it

In the index.ts file add following imports. First line imports Hono framework. Second line imports logger middleware which we will use to log HTTP requests. The third line imports core middleware which we will use to set CORS header.

import { Hono } from 'hono'
import { logger } from 'hono/logger'
import { cors } from 'hono/cors'
import 'dotenv/config'
import mongoose from "mongoose";

Add type definition for quotes and connect to MongoDB

const quoteSchema = new mongoose.Schema({
    quote: String,
    author: String
});

const Quote = mongoose.model('Quote', quoteSchema);

if(process.env.MONGO_URI) {
    await mongoose.connect(process.env.MONGO_URI)
}
else {
    console.log('MONGO_URI missing')
    process.exit()
}

Now initialize the Hono app

const app = new Hono()

Use logger middleware

app.use('*', logger())

Add CORS middleware. For development environment we will allow any origin. For production environment we limit origins that end with projects.yasakdogra.com

app.use('*', cors({
    origin: (origin) => {
        if(process.env.NODE_ENV == 'development')
            return '*'
        else {
            let url = new URL(origin)
            return url.hostname.endsWith('projects.yasakdogra.com') ? origin : ''
        }
    }
}))

To handle the actual HTTP GET request, we use app.get and pass in the relative URL and the function to handle the request.

app.get('/', async (c) => {
    const tmp = await Quote.aggregate().sample(1).exec()
    return c.json({text: tmp[0]['quote'], author: tmp[0]['author']})
})

Quote.aggregate().sample(1).exec() gets a random sample from all the quotes in the database.

c.json({text: tmp[0]['quote'], author: tmp[0]['author']}) returns json with text and author mapped to quote and author returned from the database.

Last step is to export the app, so it can be run with bun

export default app

Add scripts in package.json

{
    "scripts": {
    "dev": "NODE_ENV=development bun run --hot src/index.ts",
    "prod": "NODE_ENV=production bun run src/index.ts"
  },
}

We can now run the app in development mode with bun run dev and in production mode with bun run prod

Step 3: Hosting the API

There are several ways to host the app:

Let’s host this on a VPS with linux and docker. Copy the projects files to the linux server. Since the project is GitHub, I can use git to get the files to the server.

$ git clone https://github.com/yasakdogra/quotesapi

Create a docker file in the project folder. Replace <MONGO_URI> with the connection string we have

FROM oven/bun:latest
WORKDIR /usr/src/app
COPY package*.json bun.lockb ./
RUN bun install
ENV NODE_ENV=production
ENV PORT=3000
ENV MONGO_URI=<MONGO_URI>
COPY . .
EXPOSE 3000
CMD [ "bun", "run", "prod" ]

Build the image

$ docker build -t localhost/quotesapi .

Use the image to start a new container. Replace <PORT> with the port number you want the API to listen at

$ docker run -d \
--name quotesapi \
-p <PORT>:3000 \
--restart unless-stopped \
localhost/quotesapi

Next we can put a reverse proxy in front or port map the incoming requests directly the port. We can also setup SSL certificates for securing the connections. I have covered those in some of my other posts.

Thank you for reading. You can also check out my other projects for this series below.