Skip to content

Commit dc8363e

Browse files
authored
feat: fastify (#77)
* feat: fastify * feat: fastify * feat: fastify * feat: switch to plugin
1 parent dfdafe2 commit dc8363e

8 files changed

+523
-886
lines changed

src/server/package.json

+6-11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"name": "poor-clares-arundel-koa-server",
33
"version": "1.0.0",
4+
"type": "module",
45
"scripts": {
56
"build": "tsc",
67
"dev:build": "pnpm run build && pnpm run client",
@@ -13,24 +14,18 @@
1314
"watch": "cross-env NODE_ENV=development nodemon --watch src/**/* -e ts,tsx --exec ts-node src/server/server.ts"
1415
},
1516
"dependencies": {
17+
"@fastify/helmet": "^13.0.1",
18+
"@fastify/static": "^8.0.3",
1619
"applicationinsights": "^3.4.0",
1720
"applicationinsights-native-metrics": "^0.0.11",
21+
"fastify": "^5.2.0",
22+
"fastify-plugin": "^5.0.1",
1823
"form-data": "^4.0.1",
19-
"koa": "^2.13.4",
20-
"koa-body": "^6.0.0",
21-
"koa-helmet": "^8.0.0",
22-
"koa-router": "^13.0.0",
23-
"koa-send": "^5.0.1",
24-
"koa-static": "^5.0.0",
2524
"mailgun.js": "^10.3.0"
2625
},
2726
"devDependencies": {
2827
"@eslint/js": "^9.17.0",
29-
"@types/koa": "^2.13.5",
30-
"@types/koa-router": "^7.4.4",
31-
"@types/koa-send": "^4.1.3",
32-
"@types/koa-static": "^4.0.2",
33-
"@types/mailgun-js": "^0.22.13",
28+
"@types/node": "^22.10.5",
3429
"@typescript-eslint/eslint-plugin": "^8.0.0",
3530
"@typescript-eslint/parser": "^8.0.0",
3631
"cpx2": "^8.0.0",

src/server/pnpm-lock.yaml

+373-748
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/server/src/appInsightsPlugin.ts

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { FastifyInstance, FastifyPluginOptions } from 'fastify';
2+
import fp from 'fastify-plugin';
3+
import appInsights from 'applicationinsights';
4+
5+
// based on https://github.com/microsoft/ApplicationInsights-node.js/issues/627#issuecomment-2194527018
6+
declare module 'fastify' {
7+
export interface FastifyRequest {
8+
app: { start: number };
9+
}
10+
}
11+
12+
export const appInsightsPlugin = fp(async (fastify: FastifyInstance, options: FastifyPluginOptions) => {
13+
if (!options.client) {
14+
console.log('App Insights not configured');
15+
return;
16+
}
17+
18+
const client: appInsights.TelemetryClient = options.client;
19+
20+
fastify.addHook('onRequest', async (request, _reply) => {
21+
const start = Date.now();
22+
request.app = { start };
23+
});
24+
25+
fastify.addHook('onResponse', async (request, reply) => {
26+
if (request.raw.url === '/') return; // Ignore health check
27+
28+
const duration = Date.now() - request.app.start;
29+
client.trackRequest({
30+
name: request.raw.method + ' ' + request.raw.url,
31+
url: request.raw.url,
32+
duration: duration,
33+
resultCode: reply.statusCode.toString(),
34+
success: reply.statusCode < 400,
35+
properties: {
36+
method: request.raw.method,
37+
url: request.raw.url,
38+
},
39+
measurements: {
40+
duration: duration,
41+
},
42+
});
43+
});
44+
});

src/server/src/index.ts

+54-26
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1-
import Koa from 'koa';
2-
import helmet from 'koa-helmet';
3-
import send from 'koa-send';
4-
import serve from 'koa-static';
1+
import Fastify, { FastifyInstance } from 'fastify';
2+
import fastifyStatic from '@fastify/static';
3+
import helmet from '@fastify/helmet';
4+
55
import path from 'path';
6-
import * as appInsights from 'applicationinsights';
6+
import appInsights from 'applicationinsights';
7+
import { fileURLToPath } from 'url';
8+
9+
import { config } from './config.js';
10+
import { routes } from './routes/index.js';
11+
import { appInsightsPlugin } from './appInsightsPlugin.js';
712

8-
import { config } from './config';
9-
import { logger } from './logging';
10-
import { routes } from './routes/index';
13+
let client: appInsights.TelemetryClient | undefined;
1114

1215
if (config.appInsightsConnectionString) {
1316
appInsights
@@ -17,38 +20,63 @@ if (config.appInsightsConnectionString) {
1720
.setAutoCollectRequests(true)
1821
// .enableWebInstrumentation(true) // not being used on the client yet
1922
.start();
23+
24+
client = appInsights.defaultClient;
2025
}
2126

22-
const app = new Koa();
27+
export const fastify: FastifyInstance = Fastify({
28+
logger: true,
29+
});
30+
31+
fastify.register(appInsightsPlugin, { client });
2332

24-
app.use(
25-
helmet.contentSecurityPolicy({
33+
fastify.register(helmet, {
34+
contentSecurityPolicy: {
2635
useDefaults: true,
2736
directives: {
2837
scriptSrc: ["'self'", "'unsafe-inline'", 'storage.googleapis.com', 'www.google-analytics.com'],
2938
frameSrc: ['www.youtube.com', 'www.youtube-nocookie.com'],
3039
},
31-
})
32-
);
33-
app.use(logger);
34-
app.use(routes);
40+
},
41+
});
42+
43+
routes(fastify);
44+
45+
const __filename = fileURLToPath(import.meta.url);
46+
const __dirname = path.dirname(__filename);
3547

3648
const publicPath = path.join(__dirname, '..', 'client', 'dist');
3749

38-
app.use(serve(publicPath));
39-
app.use(async (ctx) => {
40-
await send(ctx, 'index.html', { root: publicPath });
50+
fastify.register(fastifyStatic, {
51+
root: publicPath,
52+
// prefix: '/public/',
53+
});
54+
55+
fastify.setNotFoundHandler((_req, res) => {
56+
res.sendFile('index.html');
4157
});
4258

43-
app.listen(config.port);
59+
async function start() {
60+
try {
61+
await fastify.listen({
62+
port: config.port,
63+
host: '0.0.0.0', // necessary to run in Docker https://fastify.dev/docs/latest/Guides/Getting-Started/#note
64+
});
4465

45-
console.log(
46-
`
47-
################
48-
# App started! #
49-
################
66+
const address = fastify.server.address();
67+
const port = typeof address === 'string' ? address : address?.port;
5068

51-
- server running at: http://localhost:${config.port}
69+
console.log(
70+
`# App started! #
71+
- server running at: http://localhost:${port}
5272
- static files served from: ${publicPath}
5373
`
54-
);
74+
);
75+
} catch (err) {
76+
console.error(err);
77+
fastify.log.error(err);
78+
process.exit(1);
79+
}
80+
}
81+
82+
start();

src/server/src/logging.ts

-68
This file was deleted.

src/server/src/routes/index.ts

+8-12
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
1-
import { koaBody } from 'koa-body';
2-
import Router from 'koa-router';
3-
4-
import { prayerRequestPOST } from './prayerRequestPOST';
5-
import { statusGET } from './statusGET';
6-
7-
const router = new Router();
8-
9-
router.get('/api/status', statusGET());
10-
router.post('/api/prayer-request', koaBody(), prayerRequestPOST());
11-
12-
export const routes = router.routes();
1+
import { FastifyInstance } from 'fastify';
2+
import { prayerRequestPOST } from './prayerRequestPOST.js';
3+
import { statusGET } from './statusGET.js';
4+
5+
export function routes(fastify: FastifyInstance) {
6+
statusGET(fastify);
7+
prayerRequestPOST(fastify);
8+
}

src/server/src/routes/prayerRequestPOST.ts

+30-13
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,46 @@
1+
import { FastifyInstance } from 'fastify';
12
import FormData from 'form-data';
2-
import Router from 'koa-router';
3-
import Mailgun from 'mailgun.js';
3+
import mgjs from 'mailgun.js';
44

5-
import { config } from '../config';
5+
import { config } from '../config.js';
66

7-
export function prayerRequestPOST(): Router.IMiddleware<unknown, unknown> {
8-
return async (ctx, _next) => {
9-
console.log('prayerRequestPOST');
7+
export interface IPrayerRequest {
8+
email: string;
9+
prayFor: string;
10+
}
11+
12+
export function prayerRequestPOST(fastify: FastifyInstance) {
13+
fastify.post<{ Body: IPrayerRequest }>('/api/prayer-request', async (request, reply) => {
1014
// Invokes the method to send emails given the above data with the helper library
1115
try {
12-
const { email, prayFor } = ctx.request.body;
16+
const { email, prayFor } = request.body;
17+
18+
// console.log('email:', email);
19+
// console.log('prayFor:', prayFor);
1320

1421
if (!config.apiKey || !config.domain) {
15-
throw new Error('APPSETTINGS_API_KEY and / or APPSETTINGS_DOMAIN not configured');
22+
console.error('APPSETTINGS_API_KEY and / or APPSETTINGS_DOMAIN not configured');
23+
reply.status(500);
24+
return {
25+
success: false,
26+
text: 'Your prayer request has not been sent - please try again later.',
27+
};
1628
}
1729

1830
if (!config.prayerRequestFromEmail || !config.prayerRequestRecipientEmail) {
19-
throw new Error(
31+
console.error(
2032
'APPSETTINGS_PRAYER_REQUEST_FROM_EMAIL and / or APPSETTINGS_PRAYER_REQUEST_RECIPIENT_EMAIL not configured'
2133
);
34+
reply.status(500);
35+
return {
36+
success: false,
37+
text: 'Your prayer request has not been sent - please try again later.',
38+
};
2239
}
2340

2441
// We pass the api_key and domain to the wrapper, or it won't be able to identify + send emails
2542
// const mailgun = new Mailgun({ apiKey, domain });
26-
const mailgun = new Mailgun(FormData);
43+
const mailgun = new mgjs.default.default(FormData);
2744
const mg = mailgun.client({ username: 'api', key: config.apiKey });
2845

2946
const prayerRequest = {
@@ -73,14 +90,14 @@ Your Poor Clare sisters, Arundel.`;
7390
// await mailgun.messages().send(reassuringResponse);
7491
await mg.messages.create(config.domain, reassuringResponse);
7592

76-
ctx.body = { ok: true, text: 'Thanks for sending your prayer request - we will pray.' };
93+
return { ok: true, text: 'Thanks for sending your prayer request - we will pray.' };
7794
} catch (exc) {
7895
console.error(exc instanceof Error ? exc.message : exc);
7996

80-
ctx.body = {
97+
return {
8198
success: false,
8299
text: `Your prayer request has not been sent - please try mailing: ${config.prayerRequestFromEmail}`,
83100
};
84101
}
85-
};
102+
});
86103
}

src/server/src/routes/statusGET.ts

+8-8
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
1-
import Router from 'koa-router';
1+
import { FastifyInstance } from 'fastify';
22

3-
import { config } from '../config';
3+
import { config } from '../config.js';
44

5-
export function statusGET(): Router.IMiddleware<unknown, unknown> {
6-
return async (ctx, _next) => {
5+
export function statusGET(fastify: FastifyInstance) {
6+
fastify.get('/api/status', async (_request, reply) => {
77
try {
88
console.log('statusGET');
99
const { branchName, gitSha, builtAt } = config;
10-
ctx.body = { 'branch-name': branchName, 'git-sha': gitSha, 'built-at': builtAt };
10+
return { 'branch-name': branchName, 'git-sha': gitSha, 'built-at': builtAt };
1111
} catch (exc) {
1212
console.error(exc instanceof Error ? exc.message : exc);
1313

14-
ctx.status = 500;
15-
ctx.body = {
14+
reply.status(500);
15+
return {
1616
text: `There is a problem with the server. Please try again later.`,
1717
};
1818
}
19-
};
19+
});
2020
}

0 commit comments

Comments
 (0)