Render Content For Users
In the previous module, we covered registering our Snyk App, setting up the authorization flow, and handling user authorization within our App. All of those topics are integral to the functionality of every Snyk App, but they're all what you might call "behind the scenes" topics.
In this module, we'll switch gears to focus on displaying content to the users who have authorized with our Snyk App. Specifically, we want to show unauthorized users a big button they can click to authorize and authorized users a list of their projects from Snyk.

Add a template engine to the Snyk App

While Express is perfectly capable of printing content to the screen and even rendering HTML server-side, life is much easier when using a template engine. For this tutorial, we'll be using EJS.
First things first, install the node packages we'll be using for this portion of the tutorial:
1
npm install --save ejs
Copied!
Next, we'll modify the initGlobalMiddlewares() function we created in our last module to tell express that we want to use a view engine, EJS in this case, and let it know where we'll be storing our view templates. We'll be storing our EJS templates in ./src/views and keeping any common files (e.g., images, CSS, etc...) in /.src/public.
Create the new directories first.
1
mkdir -p ./src/views/partials
2
mkdir -p ./src/public
Copied!
Now we can update ./src/app.ts:
1
// ./src/app.ts
2
...
3
4
class App {
5
6
...
7
8
private initGlobalMiddlewares() {
9
10
...
11
12
this.app.set("views", path.join(__dirname, "/views"));
13
this.app.set("view engine", "ejs");
14
this.app.use('/public', express.static(path.join(__dirname, '/public')));
15
16
...
17
18
}
19
20
...
21
22
}
Copied!
For each route that we'll provide a template for, we'll need to modify the corresponding controller and ensure that we're using res.render("<template name>") rather than something more simplistic like res.send().
E.g.,
1
...
2
3
private initRoutes() {
4
this.router.get(`${this.path}`, this.indexPage);
5
}
6
private indexPage(req: Request, res: Response, next: NextFunction) {
7
// THIS right here is what causes Express to render the EJS template.
8
return res.render("index");
9
}
10
11
...
Copied!
That's really all there is to it.
EJS templates support the concept of partial inclusion. While not strictly necessary, it makes sense to add a subdirectory to our ./src/views to differentiate partial templates like headers and footers from route templates. For the tutorial, we'll use ./src/views/partials to store such templates.

Basic EJS templates

The first template we'll create is a partial, which we'll include in the other templates. This header.ejs will be the place we link stylesheets and other information that belongs in the <head> of an HTML document.
1
// ./views/partials/header.ejs
2
3
<!DOCTYPE html>
4
<html lang="en">
5
<head>
6
<meta charset="UTF-8" />
7
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
8
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
9
<link rel="preconnect" href="https://fonts.googleapis.com" />
10
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
11
<link href="https://fonts.googleapis.com/css2?family=Roboto:[email protected];500&display=swap" rel="stylesheet" />
12
<link href="https://raw.githubusercontent.com/snyk/snyk-apps-demo/main/src/public/css/styles.css" />
13
<link rel="shortcut icon" href="https://raw.githubusercontent.com/snyk/snyk-apps-demo/main/src/public/images/snyk_dog.svg" type="image/x-icon" />
14
15
<title>Snyk Apps Tutorial</title>
16
</head>
17
</html>
Copied!
This index.ejs template will cover our basic / route.
1
// ./views/index.ejs
2
3
<%- include('./partials/header.ejs') %>
4
5
<body>
6
<div class="index-page">
7
<img class="index-page__snyk-logo" src="https://github.com/snyk/snyk-apps-demo/raw/main/src/public/images/snykLogoWithDog.svg" alt="snyk-logo" />
8
<div class="index-page__card">
9
<h1 class="index-page__title">Add Demo App</h1>
10
<p class="index-page__description">Authorize this App to connect to your Snyk account.</p>
11
<button class="button" onclick="location.href='/auth';">Install App</button>
12
</div>
13
</div>
14
</body>
Copied!
callback.ejs will render for successful user authorizations.
1
// ./views/callback.ejs
2
3
<%- include('./partials/header.ejs') %>
4
<body>
5
<div>
6
<h2 class="main__heading">Snyk Apps Tutorial</h2>
7
</div>
8
<div class="card__30">
9
<div class="callback-page__success-box">
10
<img class="snyk-con-img" src="https://github.com/snyk/snyk-apps-demo/raw/main/src/public/images/success_check.svg" alt="success" />
11
<div>
12
<h2 class="callback-page__success-text">Successfully connected to Snyk!</h2>
13
</div>
14
<button class="button" onclick="location.href='/projects';">List Projects</button>
15
</div>
16
</div>
17
</div>
18
</body>
Copied!
The above templates should be enough to get you started adding your own templates to any new routes you create. If you intend to continue using EJS, make sure to reference the documentation for information about the features offered.
Rendering content for your Snyk App can be as simple or complex as you'd like it to be. Because we're dealing with JavaScript, the options are very flexible!

Showing users a list of projects

Now that we've got some basic templates, let's take a look at how we can add some functionality to our Snyk App using a User's Snyk data. For this tutorial, we'll be setting up our app to allow users to view all of their projects within Snyk from within our app.
This is a basic, but easily extendable feature.
We'll need to create:
  • A new route controller
  • A function (or functions) to pull the project data
  • An EJS template for showing the projects
Let's start with the API work. We'll utilize the callSnykApi() function we created in the previous module. Since this directly relates to a particular route, we'll store this file with its controller. Following the patteren we've used throughout these tutorial modules, we'll create both files at ./src/routes/projects/.
1
// ./src/routes/projects/projectsHandler.ts
2
3
import { readFromDb } from "../../util/DB";
4
import { callSnykApi } from "../../util/apiHelpers";
5
import { EncryptDecrypt } from "../../util/encrypt-decrypt";
6
import { AuthData } from "../../interfaces/DB";
7
import { APIVersion } from "../../interfaces/API";
8
import { ENCRYPTION_SECRET } from "../../app";
9
10
/**
11
* Get projects handler that fetches all user projects
12
* from the Snyk API using user access token. This for
13
* example purposes. In production it will depend on your
14
* token scopes on what you can and can not access
15
* @returns List of user project or an empty array
16
*/
17
export async function getProjectsFromApi(): Promise<unknown[]> {
18
// Read data from DB
19
const db = await readFromDb();
20
const data = mostRecent(db.installs);
21
// If no data return empty array
22
if (!data) return [];
23
24
// Decrypt data(access token)
25
const eD = new EncryptDecrypt(ENCRYPTION_SECRET as string);
26
const access_token = eD.decryptString(data?.access_token);
27
const token_type = data?.token_type;
28
// Call the axios instance configured for Snyk API v1
29
const result = await callSnykApi(
30
token_type,
31
access_token,
32
APIVersion.V1
33
).post(`/org/${data?.orgId}/projects`);
34
35
return result.data.projects || [];
36
}
37
38
/**
39
*
40
* @param {AuthData[]} installs get most recent install from list of installs
41
* @returns the latest install or void
42
*/
43
export function mostRecent(installs: AuthData[]): AuthData | void {
44
if (installs) {
45
return installs[installs.length - 1];
46
}
47
return;
48
}
Copied!
Next we'll write the route controller. Follow the pattern: ./src/routes/projects/projectsController.ts.
1
// ./src/routes/projects/projectsController.ts
2
3
import type { Controller } from "../../interfaces/Controller";
4
import type { NextFunction, Request, Response } from "express";
5
import { Router } from "express";
6
import { getProjectsFromApi } from "./projectsHandler";
7
8
export class ProjectsController implements Controller {
9
// The base URL path for this controller
10
public path = "/projects";
11
// Express router for this controller
12
public router = Router();
13
14
constructor() {
15
this.initRoutes();
16
}
17
18
private initRoutes() {
19
// The route to render all user projects lists
20
this.router.get(`${this.path}`, this.getProjects);
21
}
22
23
private async getProjects(req: Request, res: Response, next: NextFunction) {
24
try {
25
const projects = await getProjectsFromApi();
26
return res.render("projects", {
27
projects,
28
});
29
} catch (error) {
30
return next(error);
31
}
32
}
33
}
Copied!
Whenever we add a new route controller, we need to update ./index.ts to include it.
1
// ./src/index.ts
2
3
import IndexController from "./routes/index/indexController";
4
import AuthController from "./routes/auth/authController";
5
import CallbackController from "./routes/callback/callbackController";
6
import ProjectsController from "./routes/projects/projectsController";
7
import App from "./app";
8
9
new App([
10
new IndexController(),
11
new AuthController(),
12
new CallbackController()
13
new ProjectsController()],
14
3000
15
);
Copied!

Wrap-up

Using the projects API handler and controller we created in this module, you should have all you need to create your own custom code and make your Snyk App do whatever you'd like it to do.
We used the v1 API here, but make sure to keep an eye out on Snyk's V3 API over the next months as more and more features are added, you may find new or more efficient endpoints to use in your Snyk App!
Last modified 7d ago