mirror of
https://github.com/movie-web/backend.git
synced 2025-09-13 17:03:26 +00:00
web server setup
This commit is contained in:
12
src/config/fragments/dev.ts
Normal file
12
src/config/fragments/dev.ts
Normal 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,
|
||||
},
|
||||
};
|
5
src/config/fragments/types.ts
Normal file
5
src/config/fragments/types.ts
Normal 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
20
src/config/index.ts
Normal 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
29
src/config/schema.ts
Normal 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({}),
|
||||
});
|
25
src/main.ts
25
src/main.ts
@@ -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);
|
||||
});
|
||||
|
97
src/modules/fastify/index.ts
Normal file
97
src/modules/fastify/index.ts
Normal 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);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
6
src/modules/fastify/routes.ts
Normal file
6
src/modules/fastify/routes.ts
Normal 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
7
src/routes/hello.ts
Normal 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
102
src/services/logger.ts
Normal 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();
|
Reference in New Issue
Block a user