Github Backend API server which collect & return data from Github & caches it in REDIS.
Install node modules: npm install
docker pull redis
docker run -p PORT:PORT --name redis-sample -d redis
Example: docker run -p 6379:6379 --name redis-sample -d redis
Note: After you run this command please save the container ID to use it whenever you want to run the same container again.
To run the same container again: docker start your_id
Please follow the .env.example file.
Your .env should contain the following elements:
CLIENT_SECRET="your_github_client_secret"
CLIENT_SECRET
is your github client token, useful for making API calls on behalf of your github account.
You can generate one here under personal access tokens
.
PORT=7000
PORT
the express port. Make sure to put the same port you will use later in the frontend. ie: 7000
REDIS_PORT=6379
REDIS_PORT
is the port of the running docker image. Please make sure to put the same port you used in the redis
docker image. ie: 6379
REDIS_HOST="127.0.0.1"
REDIS_HOST
is the host url of your local machine. ie: 127.0.0.1
Run the code with auto restart: npm run dev
Run the code with auto restart: npm run test
Compile the code into javascript files: npm run build
Executes your generated build files: npm run start
Lint the application: npm run lint:fix
After running the development server by npm run dev
, go to the following url http://locahost:7000/swagger
(with 7000 is the port you set in the step 1.2)
to browse the api endpoints documentation and examples.
Writing a Github Backend API server which eventually collect the data from Github & caches it in REDIS.
- Receives a POST request with search type(users or repositories) & search text(mandatory).
- The results will be fetched from the GitHub API & cache it for at least 2 hours.
- Clears the Backend Caching
This project was build with the following dependencies:
- express Minimalist web framework for nodejs.
- body-parser Middleware that parses incoming request bodies into req.body.
- cors Middleware used to enable Cross-origin resource sharing from the server domain to outside domains.
- @octokit/core A well tested and used extendable client for GitHub's REST & GraphQL APIs.
- redis Client to interact with the redis in memory data server.
- joi A schema description language and data validator for javascript.
- lodash A javaScript utility library delivering modularity, performance & extras.
- eslint A tool for identifying, reporting and fixing patterns found in javascript.
- camelcase-keys Useful for converting object keys to camel case.
- jest Javascript testing framework.
- supertest Provides high-level abstraction for testing HTTP endpoints
- swagger-ui-express Allows serving auto-generated swagger-ui docs based on a swagger.json file.
| src/
|- controllers/
|- cache/
|- github/
|- lib/
|- redis/
|- github/
|- services/
|- search/
|- types/
|- utils/
|- app.ts
|- index.ts
| test
-
index.ts : is the main file of the application, it runs the http server of our express app.
-
app.ts : is the express application, it uses the applyRoutes helper defined in utils/ to use an array of routes defined in controllers/
-
controllers/: is a folder containing the app routes grouped in sub-folders based on the route's context, each route contains the call to a service and exports the handler, path (i.e. /api/search...) and type (i.e. GET, POST...) to the applyRoutes helper.
By combining the traditional routes/ and /controllers/ folders into this pattern, it will be easier to make a global view on the controller handler, method and path without looking somewhere else.
-
services/: This folder contains the app's business layer divided into multiple sub-folders based on the context. It basically combines native functions found in the (lib/) folder to output the desired controller requirements.
-
lib/: Contains native calls to database, in memory cache or external apis.
-
utils/: Contains of subset of helper functions & constants, ... to be used everywhere in the server.
-
types/: This is where the typescript interfaces, types are defined.
This endpoint requires a body containing a type and query to retrieve a list of github users or repositories.
Initially, if the frontend calls this endpoint, the services will check if there's any data in the redis cache by the provided type and query combination:
- If there's a cached data, it will be returned directly.
- If no cached data is found, the service will call the github client to affect a GET /{type}&q={query} with the type being an enum between users | repositories, and the query is a string.
- If the search data returned from github, cache it in redis for 2 hours and return the data to the frontend
The cache architecture is based on simple key value pairs.
If the user searched for a query = "fakeUser" and type = "users", it will be stored this way
> SET KEY: "users_fakeUser"
VALUE: "{stringified object of the users...}"
> SET EXP: 60*60*2 => will expire in 2 hours
If the user searched for a query = "react" and type = "repositories", it will be:
> SET KEY: "repositories_fakeUser"
VALUE: "{stringified object of the repositories...}"
> SET EXP: 60*60*2 => will expire in 2 hours
The overall redis database will contain something like this:
users_karim: "{ incompleteResults: true, itemsCount: 95, items: [ item1, item2 ...] }"
repositories_react: "{ incompleteResults: true, itemsCount: 120, items: [item1... ] }"
users_alice: "{ incompleteResults: false, itemsCount: 10, items: [...] }"
repositories_eslint: "{ incompleteResults: true, itemsCount: 10, items: [...] }"
...
When the app request retrieving a cached query, we will make a native get to the redis client querying {type}_{query}
Maybe this seems like a hacky way to achieve the caching, I tried using a HMSET, this allows storing multiple values into a single key:
| repositories:
|query: value
|query: value
| users:
|query: value
|query: value
The problem with this solution is that we can't expire single query: value pair inside a key without expiring the entire key. Another solution is using redis databases, but it seems like a deprecated feature.
This simply calls the redis's flushall() method to drop the entire cache.
A few tests were provided to test the different functionalities of the app:
-
cacheRedisData.test.ts: This test basically stores an example of a search response in redis and checks that the example is stored and matches the initial data.
-
searchAndCache.test.ts: This test will check the search service as described in 3.3.
-
searchApi.test.ts: This test will check the high-level API implementation of the GET /api/search data and validation.
if I were to spend additional time on the project, I would work on improving the typescript types, and the returned data schema.