Skip to content

Commit 6f4ae02

Browse files
authored
Health check endpoint (#19)
1 parent e1cfef4 commit 6f4ae02

13 files changed

Lines changed: 115 additions & 9 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ Each key contains 6 characters.
154154
* Check URL: Check where a shortened URL redirects to
155155

156156
# TODO
157+
* XSS sanitisation
157158
* URL shortening: Check if URL is stored in the database. If it is, return the short url
158159
* Key generation: do not check against the database for existing short urls because there can still be a conflict due to concurrency. Let the database handle the conflict
159160
* Handle expired keys

src/controllers/index.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,41 @@ import {
44
// Extend from this to define a valid schema type/interface
55
} from 'express-joi-validation';
66
import { ShortenUrlRequest } from './types';
7-
import { ShortenUrlSchema } from '../controllers/validators';
7+
import { ShortenUrlSchema, HealthCheckSchema } from '../controllers/validators';
88
import { UrlShortenerServiceImpl } from '../services/impl/urlShortener';
99

10+
import { UrlDaoImpl } from '../db/postgres/dao/urlDao';
11+
import { Redis } from '../db/redis';
12+
import logger from '../utils/logger';
13+
14+
export const healthCheck = async (
15+
req: ValidatedRequest<HealthCheckSchema>,
16+
res: Response
17+
) => {
18+
const response: any = {
19+
message: 'Service is up and running!',
20+
};
21+
22+
if (req.query.postgres !== undefined) {
23+
try {
24+
await new UrlDaoImpl().healthCheck();
25+
response.postgres = 'Postgres is up and running!';
26+
} catch (e) {
27+
logger.error(`postgres health check failed: ${e}`);
28+
}
29+
}
30+
if (req.query.redis !== undefined) {
31+
try {
32+
await Redis.getInstance().ping();
33+
response.redis = 'Redis is up and running!';
34+
} catch (e) {
35+
logger.error(`redis health check failed: ${e}`);
36+
}
37+
}
38+
39+
res.json(response);
40+
};
41+
1042
export const shortenUrl = async (
1143
req: ValidatedRequest<ShortenUrlSchema>,
1244
res: Response

src/controllers/validators.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,15 @@ export interface ShortenUrlSchema extends ValidatedRequestSchema {
2323
alias?: string;
2424
};
2525
}
26+
27+
export const healthCheckValidator = Joi.object({
28+
postgres: Joi.string().min(0),
29+
redis: Joi.string().min(0),
30+
});
31+
32+
export interface HealthCheckSchema extends ValidatedRequestSchema {
33+
[ContainerTypes.Query]: {
34+
postgres?: string;
35+
redis?: string;
36+
};
37+
}

src/db/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@ export const initDb = () => {
1616
};
1717

1818
export const closeDb = async () => {
19-
await flushDb();
19+
await clearDb();
2020
await knex.destroy();
2121
await Redis.close();
2222
};
2323

24-
export const flushDb = async () => {
24+
export const clearDb = async () => {
2525
await knex('url').del();
2626
await Redis.flush();
2727
};

src/db/postgres/dao/urlDao.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
1+
import { raw } from 'objection';
12
import { Url } from '../models/url';
23

34
export interface UrlDao {
5+
healthCheck(): Promise<any>;
46
insert(id: string, originalUrl: string): Promise<void>;
57
findById(id: string): Promise<Url>;
68
findByIds(ids: string[]): Promise<Url[]>;
79
}
810

911
export class UrlDaoImpl implements UrlDao {
12+
public async healthCheck(): Promise<any> {
13+
return Url.query().select(raw('1'));
14+
}
1015
public async insert(id: string, originalUrl): Promise<void> {
1116
await Url.query().insert({
1217
id,

src/db/redis/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import config from '../../config';
66
BluebirdPromise.promisifyAll(redis);
77

88
export interface MemoryStore {
9+
ping(): Promise<any>;
910
setString(key: string, value: string, ttl?: number): Promise<void>;
1011
getString(key: string): Promise<string | null>;
1112
}
@@ -30,6 +31,10 @@ export class Redis implements MemoryStore {
3031
return redis.createClient(options);
3132
}
3233

34+
public async ping(): Promise<any> {
35+
return this.conn.ping();
36+
}
37+
3338
public async setString(
3439
key: string,
3540
value: string,

src/routes/index.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ import express, { Request, Response } from 'express';
22
import cors from 'cors';
33
import * as middlewares from '../middlewares';
44
import * as controllers from '../controllers';
5-
import { shortenUrlValidator, validator } from '../controllers/validators';
5+
import {
6+
validator,
7+
shortenUrlValidator,
8+
healthCheckValidator,
9+
} from '../controllers/validators';
610
import { corsCheck } from '../utils/cors';
711

812
const router = express.Router();
@@ -11,10 +15,19 @@ router.get(
1115
'/',
1216
cors(),
1317
middlewares.catchErrors(async (req: Request, res: Response) => {
14-
res.json('Service is up and running!');
18+
res.json({
19+
message: 'Service is up and running!',
20+
});
1521
})
1622
);
1723

24+
router.get(
25+
'/healthz',
26+
cors(),
27+
validator.query(healthCheckValidator),
28+
middlewares.catchErrors(controllers.healthCheck)
29+
);
30+
1831
router.options('/urls', corsCheck());
1932
router.post(
2033
'/urls',

src/utils/error.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { ThrowError, CustomError } from '../types/error';
22

33
export enum ErrorCode {
4+
OK = 200,
45
BAD_REQUEST = 400,
56
UNAUTHORIZED = 401,
67
FORBIDDEN = 403,

test/dbhelper.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@ import chai from 'chai';
22
import chaiAsPromised from 'chai-as-promised';
33
chai.use(chaiAsPromised);
44

5-
import { initDb, closeDb, flushDb } from '../src/db';
5+
import { initDb, closeDb, clearDb } from '../src/db';
66

77
before(async () => {
88
await initDb();
99
});
1010

1111
afterEach(async () => {
12-
await flushDb();
12+
await clearDb();
1313
});
1414

1515
after(async () => {

test/integration/healthCheck.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { expect } from 'chai';
2+
import { Server } from 'http';
3+
4+
import app from '../../src/app';
5+
import config from '../config';
6+
import { ErrorCode } from '../../src/utils/error';
7+
import { sendRequest } from './utils/sendRequest';
8+
9+
let server: Server;
10+
11+
describe('Health check', () => {
12+
beforeEach(async () => {
13+
return new Promise((resolve) => {
14+
server = app.listen(config.port, resolve);
15+
});
16+
});
17+
18+
afterEach(async () => {
19+
return new Promise((resolve, reject) => {
20+
server.close((err) => {
21+
if (err) {
22+
reject(err);
23+
}
24+
resolve();
25+
});
26+
});
27+
});
28+
29+
it('Health check should succeed for postgres and redis', async () => {
30+
const res = await sendRequest(server, 'get', `/healthz?postgres&redis`);
31+
expect(res.status).to.equal(ErrorCode.OK);
32+
expect(res.body.payload).to.eql({
33+
message: 'Service is up and running!',
34+
postgres: 'Postgres is up and running!',
35+
redis: 'Redis is up and running!',
36+
});
37+
});
38+
});

0 commit comments

Comments
 (0)