'Serving public and private ports using Nestjs
I'm building a that aims to serve a mobile application. Besides serving the client, it will have several back-office functionalities.
We are using swagger and we do want to be able to access the swagger docs of our back-office endpoints. However, we do not want to expose all of our endpoints publicly.
Assuming that having all endpoints public is a bad option one solutions we are thinking of is letting our server serve two ports, and then only exposing one port to the public. We have created a small sample repo that that serves a client module and a back-office module on two different ports.
The main.ts looks like the following:
import { NestFactory } from '@nestjs/core';
import { ClientModule } from './modules/client/client.module';
import * as express from 'express';
import * as http from 'http';
import {ExpressAdapter} from '@nestjs/platform-express';
import { BackOfficeModule } from './modules/backoffice/backoffice.module';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
async function bootstrap() {
const clientServer = express();
const clientApp = await NestFactory.create(
ClientModule,
new ExpressAdapter(clientServer),
);
const clientOptions = new DocumentBuilder()
.setTitle('ClientServer')
.setDescription('The client server API description')
.setVersion('1.0')
.addTag('client')
.build();
const clientDocument = SwaggerModule.createDocument(clientApp, clientOptions);
SwaggerModule.setup('api', clientApp, clientDocument);
await clientApp.init();
const backOfficeServer = express();
const backOfficeApp = await NestFactory.create(
BackOfficeModule,
new ExpressAdapter(backOfficeServer),
);
const backOfficeOptions = new DocumentBuilder()
.setTitle('BackOffice')
.setDescription('The back office API description')
.setVersion('1.0')
.addTag('backOffice')
.build();
const backOfficeDocument = SwaggerModule.createDocument(backOfficeApp, backOfficeOptions);
SwaggerModule.setup('api', backOfficeApp, backOfficeDocument);
await backOfficeApp.init();
http.createServer(clientServer).listen(3000); // The public port (Load balancer will route traffic to this port)
http.createServer(backOfficeServer).listen(4000); // The private port (Will be accessed through a bastian host or similar)
}
bootstrap();
Another option would be to create a bigger separation of the codebase and infrastructure, however as this is a very early stage we feel that is unnecessary.
Our question to the Nest community is thus, has anyone done this? If so, what is are your experience? What are the drawbacks to separating our backend code like this?
Solution 1:[1]
That's ok, but if you want to run two servers on 1 host, I would recommend to create two files like main-client.ts and main-back-office.ts and run them in different processes, because in that case failures of one server would not affect work of another.
Also if you are not run this in Docker I would suggest tools like forever, pm2, supervisor or my own very small library workers-cluster
If you run it in Docker and don't want big refactoring, I would recommend to create
single Dockerfile with running different CMD or ENTRYPOINT commands
Solution 2:[2]
Disclaimer: this solution is for express+REST combination.
Routing
Even thought nestjs can't separate controller's based on port, it can separate them based on host. Using that, you can add a reverse proxy in front of your application, that modifies the host header based on the port. Or, you can do that in an express middleware, to make things even more simpe. This is what I did:
async function bootstrap() {
const publicPort = 3000
const privatePort = 4000
const server = express()
server.use((req, res, next) => {
// act as a proper reverse proxy and set X-Forwarded-Host header if it hasn't been set
req.headers['x-forwarded-host'] ??= req.headers.host
switch (req.socket.localPort) {
case publicPort:
req.headers.host = 'public'
break
case privatePort:
req.headers.host = 'private'
break
default:
// this shouldn't be possible
res.sendStatus(500)
return
}
next()
})
const app = await NestFactory.create(AppModule, new ExpressAdapter(server))
http.createServer(server).listen(publicPort)
http.createServer(server).listen(privatePort)
}
Controllers:
@Controller({ path: 'cats', host: 'public' })
export class CatsController {...}
@Controller({ path: 'internal' host: 'private' })
export class InternalController {...}
Alternatively, you can simplify by creating your own PublicController and PrivateController decorators:
// decorator for public controllers, also sets guard
export const PublicController = (path?: string): ClassDecorator => {
return applyDecorators(Controller({ path, host: 'public' }), UseGuards(JwtAuthGuard))
}
// decorator for private controllers
export const PrivateController = (path?: string): ClassDecorator => {
return applyDecorators(Controller({ path, host: 'private' }))
}
@PublicController('cats')
export class CatsController {...}
@PrivateController('internal')
export class InternalController {...}
Swagger
For swagger, SwaggerModule.createDocument has an option "include", which accepts a list of modules to include in the swagger docs. With a bit of effort we can also turn the swagger serving part into an express Router, so both the private and public swagger can be served on the same path, for the different ports:
async function bootstrap() {
const publicPort = 3000
const privatePort = 4000
const server = express()
server.use((req, res, next) => {
// act as a proper reverse proxy and set X-Forwarded-Host header if it hasn't been set
req.headers['x-forwarded-host'] ??= req.headers.host
switch (req.socket.localPort) {
case publicPort:
req.headers.host = 'public'
break
case privatePort:
req.headers.host = 'private'
break
default:
// this shouldn't be possible
res.sendStatus(500)
return
}
next()
})
const app = await NestFactory.create(AppModule, new ExpressAdapter(server))
// setup swagger
let publicSwaggerRouter = await createSwaggerRouter(app, [CatsModule])
let privateSwaggerRouter: await createSwaggerRouter(app, [InternalModule])
server.use('/api', (req: Request, res: Response, next: NextFunction) => {
switch (req.headers.host) {
case 'public':
publicSwaggerRouter(req, res, next)
return
case 'private':
privateSwaggerRouter(req, res, next)
return
default:
// this shouldn't be possible
res.sendStatus(500)
return
}
})
http.createServer(server).listen(publicPort)
http.createServer(server).listen(privatePort)
}
async function createSwaggerRouter(app: INestApplication, modules: Function[]): Promise<Router> {
const swaggerConfig = new DocumentBuilder().setTitle('MyApp').setVersion('1.0').build()
const document = SwaggerModule.createDocument(app, swaggerConfig, { include: modules })
const swaggerUi = loadPackage('swagger-ui-express', 'SwaggerModule', () => require('swagger-ui-express'))
const swaggerHtml = swaggerUi.generateHTML(document)
const router = Router()
.use(swaggerUi.serveFiles(document))
.get('/', (req: Request, res: Response, next: NextFunction) => {
res.send(swaggerHtml)
})
return router
}
Solution 3:[3]
The NestJS docs cover how to let one server serve multiple ports: https://docs.nestjs.com/faq/multiple-servers#multiple-simultaneous-servers
The following recipe shows how to instantiate a Nest application that listens on multiple ports (for example, on a non-HTTPS port and an HTTPS port) simultaneously.
const httpsOptions = { key: fs.readFileSync('./secrets/private-key.pem'), cert: fs.readFileSync('./secrets/public-certificate.pem'), }; const server = express(); const app = await NestFactory.create( ApplicationModule, new ExpressAdapter(server), ); await app.init(); http.createServer(server).listen(3000); https.createServer(httpsOptions, server).listen(443);
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | |
| Solution 2 | Kicsi |
| Solution 3 | nikolockenvitz |
