Sébastien Loix
Nov 27, 2018 · 8 min read

This is the third part of a multipart tutorial on how to build a blog application in Node.js using the Google Datastore and deploy it to Google App Engine. If you missed the beginning, jump to the first part where I explain how to set up the project and where you’ll find the links to the other parts.

In this post, we will create the Application Context. An object that will contain our database access (the gstore-node instance), Google Storage instance, our app configuration from the last post and a logger instance. This context object will then be provided to each layer and module that requires it. Let’s dive into it!

Context Type

Let’s first create our Context Type and declare the 4 properties that will define it. Open the “models.ts” file at the root and add the following:

// models.tsimport Storage from '@google-cloud/storage';
import { Gstore } from 'gstore-node';
import { Logger } from 'winston';
import { Config } from './config/index';
import { BlogModule } from './modules/blog';
...export type Context = {
gstore: Gstore;
storage: Storage;
logger: Logger;
config: Config;
};
export type AppModules = {
...

Logger

Let’s now create the first property of our context: a logger instance. Logs are a very important part of an application, for this reason, we will use the winston log library. It is quite powerful and I invite you to read its documentation for more advanced scenarios. Create a logger.ts file at the root and add the following:

// logger.tsimport { createLogger, format, transports, Logger } from "winston";
import { LoggerConfig } from "./config/logger";
const { combine, timestamp, printf } = format;export default ({ config }: { config: LoggerConfig }): Logger => {
const myFormat = printf(info => {
return `${info.timestamp} ${info.level}: ${info.message}`;
});
const logger = createLogger({
level: config.level,
format: combine(timestamp(), myFormat),
transports: [
new transports.Console({ format: format.simple() })
]
});
return logger;
};

As you can see, to create a logger instance we need to provide a LoggerConfig object. We then create a custom format function for the logs that we then provide to the createLogger() method. Finally, we return the logger instance.
Let’s now create the instance in our “index.ts” file at the root (“src/server”) folder.

We first need to import our app config object and then initiate the Logger by passing its configuration.

// index.tsprocess.env.NODE_ENV = process.env.NODE_ENV || "development";import config from "./config"; // add this line
import initLogger from "./logger"; // add this line
import initApp from "./app";
// Create logger instance, providing its configuration
const logger = initLogger({ config: config.logger });
...

And now that we have imported our application config and have a logger instance, let’s replace the block at the end of the file that starts with app.listen(3000, () => { ... } by:

/**
* Start the server
*/
logger.info("Starting server...");
logger.info(`Environment: "${config.common.env}"`);
app.listen(config.server.port, (error: any) => {
if (error) {
logger.error("Unable to listen for connection", error);
process.exit(10);
}
logger.info(
`Server started and listening on port ${config.server.port}`
);
});

Database (gstore-node)

Let’s now instantiate gstore. Create a “db.ts” file at the root and add the following:

// db.tsimport Datastore from "@google-cloud/datastore";
import GstoreNode, { Gstore } from "gstore-node";
import { Logger } from "winston";
import { GcloudConfig } from "./config/gcloud";
export default ({
config,
logger
}: {
config: GcloudConfig,
logger: Logger
}): Gstore => {
logger.info(
`Instantiating Datastore instance for project "${
config.projectId
}"`
);
/**
* Create a Datastore client instance
*/
const datastore = new Datastore({
projectId: config.projectId,
namespace: config.datastore.namespace
});
/**
* Create gstore instance
*/
const gstore = GstoreNode({ cache: true });
/**
* Connect gstore to the Google Datastore instance
*/
logger.info("Connecting gstore-node to Datastore");
gstore.connect(datastore);
return gstore;
};

We can see that to create the gstore instance we need to provide an object with a GcloudConfig and a Logger instance. The code is self-explanatory, we then create a Google Datastore instance from the @google-cloud/datastore library and connect gstore to it. You probably have noticed that we activated the cache when instantiating gstore. This will add a nice performance boost by using a LRU memory cache for Key and Query fetching. Look at the documentation for the different settings or read the post I wrote about it for more information.

Just like with our logger, let’s import it in our “index.ts” file:

// index.ts...
import initLogger from "./logger";
import initDB from "./db"; // add this line
import initApp from "./app";
const logger = initLogger({ config: config.logger });
const gstore = initDB({ config: config.gcloud, logger });

Google Storage

Let’s follow the same pattern to create our @google-cloud/storage instance. Create a “storage.ts” file at the root and add the following into it:

// storage.tsimport Storage from "@google-cloud/storage";
import { GcloudConfig } from "./config/gcloud";
export default ({ config }: { config: GcloudConfig }) => {
const storage = new (Storage as any)({
projectId: config.projectId
});
return storage;
};

Again nothing fancy here. To generate our storage instance we will need to provide the GcloudConfig object. Let’s do that, open our root “index.ts” file and add the following:

// index.ts
...
import initDB from "./db";
import initStorage from "./storage"; // add this line
...
const logger = initLogger({ config: config.logger });
const gstore = initDB({ config: config.gcloud, logger });
const storage = initStorage({ config: config.gcloud }); add this
...

Great! We now have all we need to create the application context object. Let’s do that right now. Add the following right below the storage initialization:

// index.ts
...
const storage = initStorage({ config: config.gcloud });
/**
* Create App Context object
*/
const context = { gstore, storage, logger, config };
...

We can now provide this context to our app & modules initialization. This will allow us to provide it to each one of our modules later on. Open the “modules.ts” file at the root and make the following modifications:

// modules.ts...import initUtilsModule from "./modules/utils";
import { Context, AppModules } from "./models"; // edit this line
export default (context: Context): AppModules => { // edit this line
const utils = initUtilsModule();
...

Let’s do the same with our app initialization:

// app.tsimport express from "express";import { Context, AppModules } from "./models"; // add this lineexport default (context: Context, modules: AppModules) => { // edit
const app = express();
...

Let’s instantiate now both our modules and app providing our new context object. Go back to the root “index.ts” file and make the following modifications:

// index.ts
...
import initDB from "./db";
import initStorage from "./storage";
import initModules from "./modules"; // add this line
...
/**
* Create App Context object
*/
const context = { gstore, storage, logger, config };
/**
* Instantiate the modules providing our context object
*/
const modules = initModules(context); // add this line
/**
* Instantiate the Express App
*/
const app = initApp(context, modules); // edit this line
...

Express app configuration

We are almost done with the skeleton of our app. One important part is missing though: routing. We are going to look into it in a moment but first, let’s configure our Express app with a few settings. Open the “app.ts” file at the root and make the following modifications:

// app.tsimport express from "express";
import compression from "compression"; // add this line
import path from "path"; // add this line
import { Context, AppModules } from "./models";export default (context: Context, modules: AppModules) => {
const app = express();
/**
* Configure views template, static files, gzip
*/
app.use(compression());
app.set("views", "./views");
app.set("view engine", "pug");
app.use(
"/public",
express.static(path.join(__dirname, "..", "public"), {
maxAge: "1 year"
})
);
app.disable("x-powered-by");
app.use("/", (_, res) => {
res.send("Hello!");
});
return app;
};

Not much to be said here. We’ve enabled compression (gzip) to serve our application requests, defined our “views” folder and the template engine (“pug”). We’ve then defined the static folder of the server and disabled the X-Powered-By Http Header.

Routing

It is time now to handle the routes of our application. In the “src/server” folder create a “routes.ts” file and add the following:

// routes.tsimport path from "path";
import { Request, Response, NextFunction, Express } from "express";
import { Context, AppModules } from "./models";
export default (
{ logger, config }: Context,
{
app,
modules: { blog, admin }
}: { app: Express, modules: AppModules }
) => {
/**
* Web Routes
*/
app.use("/blog", (_, res) => {
res.send("Hello!");
});
/**
* 404 Page Not found
*/
app.get("/404", (_, res) => {
res.render(path.join(__dirname, "views", "404"));
});
/**
* Default route "/blog"
*/
app.get("*", (_, res) => res.redirect("/blog"));
/**
* Error handling
*/
app.use(
(err: any, _: Request, res: Response, next: NextFunction) => {
const payload = (err.output && err.output.payload) || err;
const statusCode =
(err.output && err.output.statusCode) || 500;
logger.error(payload); return res.status(statusCode).json(payload);
}
);
};

Let’s see what’s happening here. First, we declare 2 Inputs for this layer. The first one is our context object (we deconstructed it to extract the logger and config). The second one is the layer dependencies: an Express instance (app) and the AppModules (modules).

We then define the first route, “/blog”, and return Hello! like we did before. We will attach later on our BlogModule routes here.

We then create a “/404” route for when a page is not found.

We then create a default route. This will redirect “/” to “/blog”.

At last, we handle the Express errors. As we will be using Boom to handle the HTTP request errors, we check if the err object has an outputproperty and retrieve the statusCode. We then log and return the error.

Great, we only need now to attach our Express app to this routes layer and we will have the skeleton of our app! Open the “app.ts” file and import our routes layer:

// app.tsimport express from 'express';
import compression from 'compression';
import initRoutes from './routes'; // add this line

then replace the block starting with app.use('/', (_, res) => { ... } by:

initRoutes(context, { app, modules });

If you now refresh the page in your browser, you should see it redirect to the “/blog” URL.

Great! We are done with the skeleton of our app. We have defined our modules, added the application context and config, as well as some Express configuration. We finally created a routes layer and connected our app to it. This would be a good time to make a copy of this boilerplate to create other Node.js application based on this same architecture, I leave it up to you! :)

As always, if you have any question or doubt or if you spot a mistake, please reach out in the comments below. We’ll all learn from it :)

In the next section of this tutorial, we will finally get our hands dirty with some business logic and build out first module: the Blog module.
Let’s jump right into it!

Google Cloud Platform - Community

A collection of technical articles published or curated by Google Cloud Platform Developer Advocates. The views expressed are those of the authors and don't necessarily reflect those of Google.

Sébastien Loix

Written by

Software engineer @elastic • http://s.loix.me

Google Cloud Platform - Community

A collection of technical articles published or curated by Google Cloud Platform Developer Advocates. The views expressed are those of the authors and don't necessarily reflect those of Google.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade