[Confluence Cloud] ACE with NestJs?

I managed to run atlassian-connect-expresss in NestJS without modifying the library; the trick was to provide the express http adapter (after it’s been modified by atlassian-connect-express) to NestFactory.create.

This is what I used (make sure to also see the readme in atlassian-connect-express for more context):

  1. npm install -g @nestjs/cli
  2. nest new project-name; cd project-name
  3. npm install atlassian-connect-express compression cookie-parser helmet morgan errorhandler nocache --save
  4. npm install ngrok sqlite3 --save-dev
  5. Copy/modify credentials.json, config.json and atlassian-connect.json (see the readme in atlassian-connect-express)
  6. in src/main.ts:
import { NestFactory } from '@nestjs/core';
import {
  ExpressAdapter,
  NestExpressApplication,
} from '@nestjs/platform-express';
import * as atlassianConnect from 'atlassian-connect-express';
import * as compression from 'compression';
import * as cookieParser from 'cookie-parser';
import * as errorHandler from 'errorhandler';
import * as express from 'express';
import * as helmet from 'helmet';
import * as morgan from 'morgan';
import * as nocache from 'nocache';
import * as path from 'path';
import { AppModule } from './app.module';
import { AtlassianConnectService } from './atlassian-connect/atlassian-connect.service';

async function bootstrap() {
  const expressAdapter = new ExpressAdapter();

  // Atlassian security policy requirements
  // http://go.atlassian.com/security-requirements-for-cloud-apps
  // HSTS must be enabled with a minimum age of at least one year
  expressAdapter.use(
    helmet.hsts({
      maxAge: 31536000,
      includeSubDomains: false,
    }),
  );
  expressAdapter.use(
    helmet.referrerPolicy({
      policy: ['origin'],
    }),
  );

  expressAdapter.use(express.json());
  expressAdapter.use(express.urlencoded({ extended: false }));
  expressAdapter.use(cookieParser());

  // Gzip responses when appropriate
  expressAdapter.use(compression());

  // Atlassian security policy requirements
  // http://go.atlassian.com/security-requirements-for-cloud-apps
  expressAdapter.use(nocache());

  const aceInstance = atlassianConnect(expressAdapter);
  // Include atlassian-connect-express middleware
  expressAdapter.use(aceInstance.middleware());

  const app = await NestFactory.create<NestExpressApplication>(
    AppModule,
    expressAdapter,
  );
  // retrieve the express server
  const httpAdapter = app.getHttpAdapter();
  const expressApp = httpAdapter.getInstance();

  // See config.json
  const port = aceInstance.config.port();
  app.set('port', port);

  const devEnv = expressApp.get('env') === 'development';

  app.use(morgan(devEnv ? 'dev' : 'combined'));

  // Configure Handlebars
  app.setBaseViewsDir(path.join(__dirname, 'views'));
  app.setViewEngine('hbs');

  // Show nicer errors in dev mode
  if (devEnv) app.use(errorHandler());

  // Initalize the injectable NestJS Service that lets you access the instance of `atlassian-connect-express`
  const atlassianConnectService = app.get(AtlassianConnectService);
  atlassianConnectService.initialization(aceInstance);

  // Boot the HTTP server
  app.listen(port, () => {
    console.log('App server running on port' + port);
    // Enables auto registration/de-registration of app into a host in dev mode
    if (devEnv) aceInstance.register();
  });
}
bootstrap();

  1. In src/atlassian-connect/atlassian-connect.service:
import { Injectable } from '@nestjs/common';

@Injectable()
export class AtlassianConnectService {
  constructor() {}
  /**
   * `Atlassian Connect Express` (ACE) instance
   */
  private aceInstance: any;

  /**
   * Initialize the service with the `Atlassian Connect Express` (ACE) instance
   *
   * @param instance
   */
  initialization(instance) {
    this.aceInstance = instance;
  }

  getInstance() {
    return this.aceInstance;
  }
}
  1. In src/app.module.ts:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AtlassianConnectService } from './atlassian-connect/atlassian-connect.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService, AtlassianConnectService],
})
export class AppModule {}

Done! Now you can inject AtlassianConnectService and do things like

constructor(atlassianConnectService: AtlassianConnectService){}
...
const addon = this.atlassianConnectService.getInstance()
addon.authenticate()(req,res,next) // verifies that the request was sent from Atlassian, and not altered
const httpClient = addon.httpClient({ clientKey })
httpClent.post(url) // sends a post request with authentication for the specific client

Thanks to @FrankyFrank and @Farid for posting the example that I started this off of.

8 Likes