[Confluence Cloud] ACE with NestJs?

Hello Dev community,

we are new to the Connect App development and we’d like to use the NestJs framework along with the atlassian-connect-express one.

Has anyone already succeeded in using both at the time?

Thanks in advance for your feedback,

Cheers,

Farid

3 Likes

Hi @Farid, I’m a Spring/Angular developer and am looking to get into both Atlassian Connect and potentially NestJS, so basically in the same boat. Any news from your side, did you manage to combine nest and Atlassian Connect?

1 Like

Hi @mararn1618,
sorry for the late reply, I was on leave.

We did manage to make NestJS and ACE work together but we had to somehow “hack” ACE.
My business partner @FrankyFrank created our own fork of the Atlassian-Connect-Express library (Bitbucket): it still lacks a proper Readme to be shared with you but Frank is working on it :).
The main issue was to manage the registration order for middlewares and routes as ACE has its own middlewares and routes management and so does NestJS (we want all the routes to be managed by NestJS controllers).
We’ll soon share our progress with you :slight_smile:

Cheers,

Farid

4 Likes

Hi @mararn1618!

We’ve updated the README file with instructions.
The README and the ACE code adaptation are committed on the branch nestjs-adaptation that you can check here

Feel free to let us know what you think!

Frank

3 Likes

Wow, really appreciate both of you getting back! I’ll first need to wrap my head around Connect with good old Spring (safe grounds ;)), but then I’d love to try Nest. Thanks! :slight_smile:

2 Likes

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

I seemed to have tried such a solution but I did not find a way to make it work.
Awesome job @ert485!
That is very good news, I’m looking forward to getting rid of this fork.
Thanks for sharing.

2 Likes

@ert485 I also had to implement those routes on the controller:

  @Get('/')
  root(@Res() response): void {
    response.charset = 'UTF-8';

    const addon = this.atlassianConnectService.getInstance();
    response.json(addon.descriptor);
  }

  @Get('/atlassian-connect.json')
  getAtlassianConnectJson(@Res() response): void {
    response.charset = 'UTF-8';

    const addon = this.atlassianConnectService.getInstance();
    response.json(addon.descriptor);
  }

  @Post('/installed')
  installed(@Res() response): void {
    response.sendStatus(HttpStatus.OK);
  }