web server setup

This commit is contained in:
mrjvs
2023-08-23 14:40:58 +02:00
parent bfcf6d0568
commit 9166c37aea
12 changed files with 949 additions and 8 deletions

View File

@@ -0,0 +1,12 @@
import { FragmentSchema } from '@/config/fragments/types';
export const devFragment: FragmentSchema = {
server: {
cors: 'http://localhost:5173',
trustProxy: true,
},
logging: {
format: 'pretty',
debug: true,
},
};

View File

@@ -0,0 +1,5 @@
import { configSchema } from '@/config/schema';
import { PartialDeep } from 'type-fest';
import { z } from 'zod';
export type FragmentSchema = PartialDeep<z.infer<typeof configSchema>>;

20
src/config/index.ts Normal file
View File

@@ -0,0 +1,20 @@
import { devFragment } from '@/config/fragments/dev';
import { configSchema } from '@/config/schema';
import { createConfigLoader } from 'neat-config';
const fragments = {
dev: devFragment,
};
export const conf = createConfigLoader()
.addFromEnvironment('MWB_')
.addFromCLI('mwb-')
.addFromFile('.env', {
prefix: 'MWB_',
})
.addFromFile('config.json')
.addZodSchema(configSchema)
.setFragmentKey('usePresets')
.addConfigFragments(fragments)
.freeze()
.load();

29
src/config/schema.ts Normal file
View File

@@ -0,0 +1,29 @@
import { z } from 'zod';
export const configSchema = z.object({
server: z
.object({
// port of web server
port: z.coerce.number().default(8080),
// space seperated list of allowed cors domains
cors: z.string().default(''),
// should it trust reverse proxy headers? (for ip gathering)
trustProxy: z.coerce.boolean().default(false),
// prefix for where the instance is run on. for example set it to /backend if you're hosting it on example.com/backend
// if this is set, do not apply url rewriting before proxing
basePath: z.string().default('/'),
})
.default({}),
logging: z
.object({
// format of the logs, JSON is recommended for production
format: z.enum(['json', 'pretty']).default('pretty'),
// show debug logs?
debug: z.coerce.boolean().default(false),
})
.default({}),
});

View File

@@ -1 +1,24 @@
console.log("hello world")
import { setupFastify } from '@/modules/fastify';
import { scopedLogger } from '@/services/logger';
const log = scopedLogger('mw-backend');
async function bootstrap(): Promise<void> {
log.info(`App booting...`, {
evt: 'setup',
});
await setupFastify();
log.info(`App setup, ready to accept connections`, {
evt: 'success',
});
log.info(`--------------------------------------`);
}
bootstrap().catch((err) => {
log.error(err, {
evt: 'setup-error',
});
process.exit(1);
});

View File

@@ -0,0 +1,97 @@
import Fastify, { FastifyInstance } from 'fastify';
import cors from '@fastify/cors';
import { conf } from '@/config';
import { makeFastifyLogger, scopedLogger } from '@/services/logger';
import { setupRoutes } from './routes';
import {
serializerCompiler,
validatorCompiler,
} from 'fastify-type-provider-zod';
import { ZodError } from 'zod';
const log = scopedLogger('fastify');
export async function setupFastify(): Promise<FastifyInstance> {
log.info(`setting up fastify...`, { evt: 'setup-start' });
// create server
const app = Fastify({
logger: makeFastifyLogger(log) as any,
});
let exportedApp: FastifyInstance | null = null;
app.setValidatorCompiler(validatorCompiler);
app.setSerializerCompiler(serializerCompiler);
app.setErrorHandler((err, req, reply) => {
if (err instanceof ZodError) {
reply.status(400).send({
errorType: 'validation',
errors: err.errors,
});
return;
}
if (err.statusCode) {
reply.status(err.statusCode).send({
errorType: 'message',
message: err.message,
});
return;
}
log.error('unhandled exception on server:', err);
log.error(err.stack);
reply.status(500).send({
errorType: 'message',
message: 'Internal server error',
...(conf.logging.debug
? {
trace: err.stack,
errorMessage: err.toString(),
}
: {}),
});
});
// plugins & routes
log.info(`setting up plugins and routes`, { evt: 'setup-plugins' });
await app.register(cors, {
origin: conf.server.cors.split(' ').filter((v) => v.length > 0),
credentials: true,
});
await app.register(
async (api, opts, done) => {
setupRoutes(api);
exportedApp = api;
done();
},
{
prefix: conf.server.basePath,
},
);
// listen to port
log.info(`listening to port`, { evt: 'setup-listen' });
return new Promise((resolve) => {
app.listen(
{
port: conf.server.port,
host: '0.0.0.0',
},
function (err) {
if (err) {
app.log.error(err);
log.error(`Failed to setup fastify`, {
evt: 'setup-error',
});
process.exit(1);
}
log.info(`fastify setup successfully`, {
evt: 'setup-success',
});
resolve(exportedApp as FastifyInstance);
},
);
});
}

View File

@@ -0,0 +1,6 @@
import { helloRouter } from '@/routes/hello';
import { FastifyInstance } from 'fastify';
export async function setupRoutes(app: FastifyInstance) {
app.register(helloRouter);
}

7
src/routes/hello.ts Normal file
View File

@@ -0,0 +1,7 @@
import { FastifyPluginAsync } from 'fastify';
export const helloRouter: FastifyPluginAsync = async (app) => {
app.get('/ping', (req, res) => {
res.send('pong!');
});
};

102
src/services/logger.ts Normal file
View File

@@ -0,0 +1,102 @@
import { conf } from '@/config';
import { URLSearchParams } from 'url';
import winston from 'winston';
import { consoleFormat } from 'winston-console-format';
const appName = 'mw-backend';
function createWinstonLogger() {
let loggerObj = winston.createLogger({
levels: Object.assign(
{ fatal: 0, warn: 4, trace: 7 },
winston.config.syslog.levels,
),
level: 'debug',
format: winston.format.combine(
winston.format.colorize(),
winston.format.ms(),
winston.format.label({ label: appName }),
winston.format.simple(),
winston.format.padLevels(),
winston.format.errors({ stack: true }),
consoleFormat({
showMeta: false,
inspectOptions: {
depth: Infinity,
colors: true,
maxArrayLength: Infinity,
breakLength: 120,
compact: Infinity,
},
}),
),
defaultMeta: { svc: appName },
transports: [new winston.transports.Console()],
});
// production logger
if (conf.logging.format === 'json') {
loggerObj = winston.createLogger({
levels: Object.assign(
{ fatal: 0, warn: 4, trace: 7 },
winston.config.syslog.levels,
),
format: winston.format.combine(
winston.format.label({ label: appName }),
winston.format.errors({ stack: true }),
winston.format.json(),
),
defaultMeta: { svc: appName },
transports: [new winston.transports.Console()],
});
}
return loggerObj;
}
export function scopedLogger(service: string) {
const logger = createWinstonLogger();
logger.defaultMeta = {
...logger.defaultMeta,
svc: service,
};
return logger;
}
export function makeFastifyLogger(logger: winston.Logger) {
logger.format = winston.format.combine(
winston.format((info) => {
if (typeof info.message === 'object') {
const { message } = info as any;
const { res, responseTime } = message || {};
if (!res) return false;
const { request, statusCode } = res;
if (request.method === 'OPTIONS') return false;
let url = request.url;
try {
const pathParts = (request.url as string).split('?', 2);
if (pathParts[1]) {
const searchParams = new URLSearchParams(pathParts[1]);
pathParts[1] = searchParams.toString();
}
url = pathParts.join('?');
} catch {
// ignore error, invalid search params will just log normally
}
// create log message
info.message = `[${statusCode}] ${request.method.toUpperCase()} ${url} - ${responseTime.toFixed(
2,
)}ms`;
return info;
}
return info;
})(),
logger.format,
);
return logger;
}
export const log = createWinstonLogger();