The API rewrite.

Long ago, I wrote the first version of the API for the now defunct Discord Dungeons Andr...

Long ago, I wrote the first version of the API for the now defunct Discord Dungeons Android app.

At first, I was planning to write it in Python, but this quickly became a pain as I would have to basically implement the bot in Python and I decided to switch to NodeJS — the same framework we use for the bot itself.

Now, in order to protect the API from unauthorized access, I decided to go with the standard of looking at other implementations for inspiration.

After a while of looking around, I found the solution to my problem — API Keys.

And so, I implemented them.

At first, there was no real logic behind them, instead I opted to register the keys based on the application name that users had requested.

Safe to say, this was a bad idea.

So you got hacked?

No, Lauren, I didn't get hacked.

It was a bad idea because they had nothing in common, making them harder to validate.

Granted, the validation method was just me fetching the keys data from the database and looking if it existed, but it worked.

Well, what's the issue?

The issue was extensibility.

And that I had to add the keys manually.

...and that the code was hard to read.

...and the key generation.

...and that users had to manually request a key.

...

So I decided to finally do a rewrite.


In the rewrite, I wanted a few things.

Extensibility

In order to cleanly and effectively add new endpoints to the API, I needed a structured way to do so.

Cleaner Code

I wanted code that I was proud of, that I could share and that wasn't just another italian restaurant. (Also known as spaghetti code)

A dashboard UI for users

I also wanted a fully featured dashboard that users could use to get keys for their applications instead of having to send an email to the support and ask for the keys there since that was taking alot of time.

And I got working.

Starting off, I had to rewrite the core of the API since it's responsible for handling the requests and delivering the data.

Since I wasn't doing a complete rewrite of the core, I could re-use a lot of the old code for the endpoints, and since I'm using ExpressJS for the core, I could define each endpoint as it's own router and then import that into the root API router and assign everything it's route there.

I was also able to do all my permission checking as middlewares to the entire API instead of having to check it in each individual endpoint.

Then came the dashboard

Since I was going to use React for the Dashboard due it being the UI framework I'm the most familiar with, I needed a way to keep track of the application state as well as a way to do the routing for each of my features of the dashboard.

For this, I decided to use what I was most familiar with once again — React-Redux and React-Router.

Now, Redux is pretty straight forward. You create some actions and some reducers and then call the action from your React component when needed – super simple.

For example, a simple action that fetches a legal document and the reducer for it;

// Action

import { setError, setDocument } from '../reducers/Legal.reducer'

export const fetchPrivacy = () => async (dispatch, getState) => {
	try {
		const res = await fetch('/legal/privacy.md', {
			method: 'GET',
		})

		const body = await res.text()

		if (body.status !== 200) {
			dispatch(setError(body))
		} else {
			dispatch(setDocument(true))
		}

	} catch (err) {
		dispatch(setError(err))
	}
}

// Reducer

export const initialState = {
	error: null,
	done: false,
}

export const SET_ERROR = 'setError@legal'

export const setError = payload => ({
	type: SET_ERROR,
	payload,
})

export const SET_DOCUMENT = 'setDocument@legal'

export const setDocument = payload => ({
	type: SET_DOCUMENT,
	payload,
})

As for React-Router on the other hand...

It's actually a breeze to use — Got ya.

...

Or so I would like it to be. See, using React-Router itself in a standalone project is a breeze — You define your routes and what components should handle them and it does all the magic for you. At least on the front-end.

Now, as I was planning to run the dashboard from the API in order to minimize overhead and make it all easier to bundle and update, I had to make ExpressJS also recognize my routes so that you could navigate to /dash/docs directly instead of having to go to /dash then click forward to the docs route.

Wait what? What's going on?

I'm glad you asked.

See, since all the routing was done in the front-end, the ExpressJS server didn't care about your navigation once you had loaded the application, which is exactly where the issues are — loading the application.

Since I was serving the dashboard from the ExpressJS server, when you directly loaded a path it didn't recognize, you would be greeted by this error:

Now, to understand why this happens, we have to understand how the request to get the docs itself is handled.

Since the request is to the server, the server needs to handle it and then send you to the dashboard entry point to let the dashboard router take over.

What's the issue? Just make it do that.

It's not that easy, Lauren.

See, I first thought of defining every route I had in my dashboard router on the ExpressJS server, so I did.

...

That didn't turn out so well.

See, the browser was getting redirected to /dash/docs/, meaning it would try loading everything from /dash/docs/<path>, when it should've been loaded from /dash/<path>.

"Damn it", I thought to myself, as I kept trying to get it working as intended.

Many hours of googling, experimenting and frustration later, I took a step back and rethought the whole solution.

"What if I do a multi route match instead of define every route?"

And so, I did.

Instead of defining every route individually, I matched every route itself.

router.get("/dash/docs", () => {})
router.get("/dash/applications", () => {})
router.get("/dash/applications/:id/edit", () => {})
router.get("/dash/privacy", () => {})
router.get([
    '/:path*?',
    '/*',
], () => {})

Not only did this work a lot better, it also meant that I didn't have to add a route to my backend every time I added one in the frontend.

But the fun doesn't stop there!

It wouldn't be developing if everything just worked on the first try, now would it?

(The answer is no)

See, the frontend would still try to load from the relative directory, so I had to create a redirect.

router.get([
	'/:path*?',
	'/*',
], (req, res) => {
	if (req.originalUrl === '/dash') {
		res.redirect('/dash/')
	} else {

		if (req.params[0].includes('static')) {
			res.redirect(req.baseUrl + req.params[0])
		} else {
			// Send index
		}
	}
})

And from there, it was smooth sailing for the backend dashboard routing.

Now, the API dashboard isn't done yet, it's far from done, there's still a bunch of stuff to do, just check the Waffle board.

The waffle board

Regardless, it's a fun project to revisit and rework and I'm learning a bunch of new stuff from it.

Hey Lauren, you can go home now.

Uh, Lauren?

You there? No?

...

Okay then.


The new API Dashboard can be found at https://api.discorddungeons.me/dash/ and it's open to everyone!