#
Step 2 - Discovering app template
Available in beta only
This is available in beta for now. Be aware that some performance issues might occur.
This is a guide to help you to understand the Quable app template express
#
Project structure
This Quable application model is built over the MVC (Model-View-Controller) pattern to provide an organized structure, a clear separation of concerns and a more fluid and efficient development experience.
.
├── database/
│ └── schema.prisma
├── public/
│ ├── images/
│ │ └── quable-logo.png
│ ├── scripts/
│ │ ├── ui/
│ │ │ ├── error.js
│ │ │ └── loader.js
│ │ ├── utils/
│ │ │ └── custom-fetch.ts
│ │ └── index.js
│ ├── styles/
│ │ └── app.css
│ └── views/
│ ├── component/
│ │ └── banner.ejs
│ ├── layout/
│ │ └── layout.ejs
│ └── pages/
│ └── index.ejs
├── src/
│ ├── controllers/
│ │ └── app.controller.ts
│ ├── middlewares/
│ │ └── http-logger.middleware.ts
│ │ └── session.middleware.ts
│ ├── routes/
│ │ └── app.routes.ts
│ ├── services/
│ │ ├── app.service.ts
│ │ └── database.service.ts
│ ├── helper/
│ │ ├── session/
│ │ │ ├── route.ts
│ │ │ ├── auth.ts
│ │ ├── config.ts
│ │ └── types.ts
│ └── index.ts
├── .env
├── .eslintrc.json
├── .eslintignore
├── .prettierignore
├── .prettierrc
├── quable.app.yml
├── package.json
├── README.md
└── tsconfig.json
In this project, there's a database
directory containing a Prisma schema file, followed by a public
directory for public resources. In a src
folder, there are directories for controllers
, middleware
, routes
and services
. In addition, in the root of the project, there's an .env
file for environment variables, as well as configuration files for linter
and prettier
.
#
Linter & prettier
Linters and Prettier are essential tools in modern software development for maintaining code quality and consistency.
Linters analyzes source code to detect and report patterns or practices that could lead to errors or issues, enforcing coding standards and best practices. They ensure code correctness, improve readability, and identify potential bugs early in the development process. The linter configuration is defined .eslintrc.json
Prettier focuses on code formatting, automatically enforcing a consistent and aesthetically pleasing style guide. It eliminates debates over code formatting preferences and ensures that code is consistently formatted regardless of individual developers' styles. The prettier configuration is defined .prettierrc.json
#
Application configuration and environnement variables
Our project's root contains 2 essential files: the application configuration file quable.app.yml
and the environment variables file .env
:
- Application configuration file : It holds essential configuration settings for our Quable application. The
application_type
field specifies how the application should be displayed (in_app or document). Thequable_pim_scope
field determines the permission level for the application on the Quable pim. Possible value are full_access or read_access.
application_type: in_app
quable_pim_scope:
- full_access
- Environment variables file : It contains environment variables necessary for our application runtime. These variables are essential for configuring database connections (
DATABASE_URL
), specifying the application's port (QUABLE_APP_PORT
), defining the application host url (QUABLE_APP_HOST_URL
) and your partner credentials (QUABLE_PARTNER_SECRET
&QUABLE_PARTNER_ID
).
DATABASE_URL=file:./dev.db
QUABLE_APP_PORT=4000
QUABLE_APP_HOST_URL=<your-application-host>
QUABLE_PARTNER_SECRET=<your-partner-secret>
QUABLE_PARTNER_ID=<your-partner-id>
#
Index.ts
In the src
directory, there's index.ts
. It initializes a Node.js
application using Express.js
and define application routing, middleware, and runtime configurations.
The function
setupAppConfig
initializes the application's configuration by readingquable.app.yml
and applying the found configuration to the Express.js app.Security middlewares: Enabling CORS support, JSON parsing, cookie handling, and URL-encoded form data processing to ensure the security and integrity of the application.
// Security
app.use(cors());
app.use(express.json());
app.use(cookieParser());
app.use(express.urlencoded({ extended: true }));
- Application middlewares: Used in the application to enhance functionality of our application.
// Middleware
app.use(httpLoggerMiddleware);
app.use(sessionMiddleware);
- View engine and static files: prepares the application for rendering views using EJS templates, specifies the location of views and layouts, and serves static assets from the
public
directory
app.use(expressLayouts);
app.use(express.static(join(__dirname, "..", "public")));
app.set("view engine", "ejs");
app.set("views", join(__dirname, "..", "public", "views"));
app.set("layout", "layouts/layout");
- Application routers: Specifies that for the root path (
/
), we'll use theappRouter
for routing and handling incoming requests. Any request to the root URL of the application will be processed and routed according to the logic defined in theappRouter
.
app.use("/", appRouter);
#
Application middleware
Middlewares are intermediary functions used in web applications to process HTTP requests. They enable the addition of features like authentication, data validation, and error handling before requests reach the final route handlers. We have 2 middlewares a located in src/middlewares
folder:
- The
sessionMiddleware
manages user sessions and authentication for all routes. It intercepts incoming requests, distinguish between public and protected routes. Public routes are allowed without authentication, while protected routes invoke thehandleAuthAndSession
function for authentication and session management.
export async function sessionMiddleware(
req: any,
res: Response,
next: NextFunction,
) {
if (isPublicRoute(req)) {
next();
return;
}
try {
await handleAuthAndSession(req, res);
next();
} catch (error) {
res.status(401).send(`Unauthorized: ${error.message}`);
}
}
async function handleAuthAndSession(req: any, res: Response) {
let authToken = req.cookies.token;
let decoded: SessionData | undefined;
try {
if (isNewThirdPartyCall(req) || !authToken) {
authToken = generateNewAuthToken(req.query);
}
decoded = verifyAuthToken(authToken) as SessionData;
} catch (error) {
authToken = handleTokenError(error, authToken);
}
await finalizeRequest(req, res, decoded!, authToken);
}
The isPublicRoute
function determines if a route requires authentication. It also maintains a list of public routes. For example, if you want a route like /health-check
to bypass the handleAuthAndSession
process, add it to the array of unProtectedRoutes
inside this function.
The session middleware also checks that the application is correctly installed for the current instance. This verification process adds two additional properties,
customer
andquableInstance
, to the request object. These properties are accessible to your entire code base once the middleware has been executed.
- The
httpLoggerMiddleware
configures HTTP request logging with the morgan library. A custom token is used to extract the customer identifier from the request object.
export function httpLoggerMiddleware(
req: any,
res: Response,
next: NextFunction
) {
morgan.token("instance", (req: any) =>
req.customer ? `[${req.customer}]` : ""
);
const logFormat = ":instance :method :url :status :response-time ms";
const excludeRoutePattern = /\.\w+(\?.*)?$/;
if (!excludeRoutePattern.test(req.url)) {
morgan(logFormat)(req, res, next);
} else {
next();
}
}
#
Application router
Routes are important components in web applications that define the mapping between URLs and actions to be executed. They direct user requests to the appropriate controller handlers for processing and responding to requests. We have one router located in src/routes
folder:
GET /
: When a GET request is made to the root path, it is routed to theappController.renderIndexPage
method for processing.POST /
: When a POST request is made to the root path, it is routed to theappController.launchDocumentApp
method for processing. This route is typically used for launching a document type Quable app.GET /permission
: When a GET request is made to/permission
, it's routed to theappController.getQuablePIMScope
method for processing.GET /install
: When a POST request is made to "/install," it is routed to theappController.installApp
method for processing.
const appRouter = Router();
appRouter.get("/", appController.renderIndexPage);
appRouter.post("/", appController.launchDocumentApp);
appRouter.get("/permission", appController.getQuablePIMScope);
appRouter.post("/install", appController.installApp);
#
Application controller
Controllers handle actions and data processing based on user requests. They act as intermediaries between routes and the application's business logic. We have one controller located in src/controllers
folder:
renderIndexPage
: It renders an EJS view located atpages/index.ejs
and sends the rendered HTML as a response. This view is typically used to display the application's main page.getQuablePIMScope
: It's used to determine permissions scopes of the application, by returning thequable_pim_scope
value.installApp
: It callsappService.installApp
method with the request body to initiate the installation process.launchDocumentApp
: It calls theappService.launchDocumentApp
method with the request body to generate a URL for launching the document app.
public renderIndexPage = async (_req: any, res: Response) => {
return res.render('pages/index.ejs');
};
public getQuablePIMScope = async (req: any, res: Response) => {
const quablePIMScope = req.app?.settings?.quable_pim_scope;
if (quablePIMScope) {
return res.status(200).send(quablePIMScope);
} else {
return res.status(404).send('quable_pim_scope not found');
}
};
public installApp = async (req: any, res: Response) => {
const installationResponse = await appService.installApp(req.body);
return res
.status(installationResponse.statusCode)
.send(installationResponse);
};
public launchDocumentApp = async (req: any, res: Response) => {
const launchResponse = await appService.launchDocumentApp(req.body);
return res.status(launchResponse.statusCode).send(launchResponse);
};
#
Application services
Services handle business logic and data operations. They encapsulate application functionality for efficient reuse across different parts of the application.
#
1. Application service
The service class provided here encapsulates essential functionalities for managing interactions with the Quable platform within your application. It offers methods ensure the secure installation to check and save authentication tokens.
- The
checkAuthToken
method validates an authentication token by making an HTTP GET request to Quable API.
public checkAuthToken = async (instanceName: string, authToken: string) => {
try {
await axios.get(`https://${instanceName}.quable.com/api`, {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authToken}`,
},
});
return true;
} catch (error) {
return false;
}
};
- The
installApp
method manages installation of the Quable app. It validates the token usingcheckAuthToken
and updates/creates a Quable instance record in the database.
public installApp = async (payload: Record<string, any>) => {
const response = { statusCode: 201, message: 'OK', data: null };
try {
const { quableInstanceName, quableAuthToken } = payload.data;
if (!(await this.checkAuthToken(quableInstanceName, quableAuthToken))) {
response.statusCode = 500;
response.message =
'Installation failed. Please check your information.';
return response;
}
const instanceData = {
authToken: quableAuthToken,
name: quableInstanceName,
};
let instance = await databaseService.quableInstance.findFirst({
where: { name: quableInstanceName },
});
instance = instance
? await databaseService.quableInstance.update({
where: { id: instance.id },
data: instanceData,
})
: await databaseService.quableInstance.create({ data: instanceData });
response.message = `Installed on ${instance.name}.quable.com`;
} catch (error) {
response.statusCode = 500;
response.message = error.message;
}
return response;
};
- The
launchDocumentApp
ensures thatdocument
application works properly. Since document applications are triggered by a POST request and await a session URL, we'll return a new url builded with current URL parameters.
public launchDocumentApp = async (payload: Record<string, any>) => {
const response = { statusCode: 200, message: 'OK', url: '', err: 0 };
try {
const { quableInstanceName, dataLocale, interfaceLocale, userId } =
payload.data;
response.url = `${process.env.QUABLE_APP_HOST_URL}?quableInstanceName=${quableInstanceName}&dataLocale=${dataLocale}&interfaceLocale=${interfaceLocale}&userId=${userId}`;
} catch (error) {
response.statusCode = 500;
response.message = error.message;
response.err = 1;
}
return response;
};
#
2. Database service
As we mentioned earlier, we're using Prisma as the ORM for the database. At the root of the project, there's a database folder containing a schema that describes our database. This service will enable you to manipulate your database tables more easily.
class DatabaseService extends PrismaClient {
private static instance: DatabaseService;
private constructor() {
super({
datasources: {
db: {
url: process.env.DATABASE_URL,
},
},
});
}
public static getInstance(): DatabaseService {
if (!this.instance) {
this.instance = new DatabaseService();
}
return this.instance;
}
}
Done!
We've explored our application template and learned how middlewares enhance request handling, routes define URL-to-action mapping, and controllers manage actions and data processing in web applications. Understanding how these components works is essential for efficient development and navigation within the application.