Build a blog application on Google App Engine: Admin module (part 5)

Sébastien Loix
Google Cloud - Community
8 min readDec 4, 2018

This is the fifth part of a multipart tutorial on how to build a small 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 are going to build a small Admin module that will allow us to create, edit and delete BlogPosts.

Admin Routes Handlers

Let’s start by creating the handlers for the routes that we will need. Our small module will have three pages:

  • / The home page to list our posts (GET)
  • /create-post page (GET or POST)
  • /edit-post page (GET or POST)

Let’s create the handlers for those 3 routes.

Home route handler

Create an “admin.routes-handlers.ts” file inside the admin module folder and add the following:

// modules/admin/admin.routes-handlers.tsimport is from "is";
import { Request, Response } from "express";
import { Context, Modules } from "./models";
export interface AdminRoutesHandlers {
dashboard(req: Request, res: Response): any;
createPost(req: Request, res: Response): any;
editPost(req: Request, res: Response): any;
}
export default (
{ gstore }: Context,
{ blog }: Modules
): AdminRoutesHandlers => {
const { blogPostDomain } = blog.blogPost;
return {
async dashboard(req, res) {
const template = "admin/dashboard";
let posts;
try {
posts = await blogPostDomain.getPosts({
cache: req.query.cache !== "false"
});
} catch (error) {
return res.render(template, {
error,
pageId: "admin-index"
});
}
res.render(template, {
posts: posts.entities,
pageId: "admin-index",
});
},
};
};

The code for the handler of the home (dashboard) page is straightforward, we simply call the getPosts() method from the blogPost domain layer. We then enable of disable the cache based on the cache URL query parameter. This query parameter allows us to fetch the latest posts data from the Datastore after we have created or updated a post.

You’ve probably noticed that we import some types from a “models.ts” file that don’t exist yet. Let’s add them to our models.ts file.

// modules/admin/models.tsimport { Gstore } from "gstore-node";
import { Logger } from "winston";
import { BlogModule } from "../blog/index";
import { ImagesModule } from "../images/index";
export type Context = {
gstore: Gstore;
logger: Logger;
};
export type Modules = {
blog?: BlogModule;
images?: ImagesModule;
};

Create post route handler

Let’s now add the route handler to create a blog post:

// modules/admin/admin.routes-handlers.ts...export default (
{ gstore }: Context,
{ blog }: Modules
): AdminRoutesHandlers => {
const { blogPostDomain } = blog.blogPost;
return {
async dashboard(req, res) { ... },
async createPost(req, res) {
const template = "admin/edit";
if (req.method === "POST") {
const entityData = Object.assign({}, req.body, {
file: req.file
});
// We use the gstore helper to create a Dataloader instance
const dataloader = gstore.createDataLoader();
try {
await blogPostDomain.createPost(entityData, dataloader);
} catch (err) {
return res.render(template, {
blogPost: entityData,
error: is.object(err.message) ? err.message : err
});
}
// After succesfully creating the post we got back
// to the home page and disable the cache
return res.redirect("/admin?cache=false");
}
return res.render(template, {
pageId: "blogpost-edit"
});
}

We first check if the request method is GET or POST. If it is a POST, it means that we are submitting the form. In that case we create an entityData object by merging the body of the request with an optional file attached to the request (we will see in a minute how to attach a file to the HTTP request). We then use a gstore utility function to create a Dataloader instance and we call the createPost() domain method to create the BlogPost. Once the blog post has been created we redirect the user to the dashboard (and disable the cache).

For the GET request method, things are much simpler :) We simply render the view template.

Edit post route handler

Finally, let’s add the handler to edit a post. It will be very similar to the post creation so I won’t get into details. The main difference is that here, for the GET request, we do need to fetch the blog post entity and pass it to our view template.

// modules/admin/admin.routes-handlers.ts...export default (
{ gstore }: Context,
{ blog }: Modules
): AdminRoutesHandlers => {
const { blogPostDomain } = blog.blogPost;
return {
async dashboard(req, res) { ... },
async createPost(req, res) { ... },
async editPost(req, res) {
const template = "admin/edit";
const pageId = "blogpost-edit";
const dataloader = gstore.createDataLoader();
const { id } = req.params;
if (req.method === "POST") {
const entityData = Object.assign({}, req.body, {
file: req.file
});
try {
await blogPostDomain.updatePost(
id,
entityData,
dataloader,
true
);
} catch (err) {
return res.render(template, {
post: Object.assign({}, entityData, { id }),
pageId,
error: is.object(err.message) ? err.message : err
});
}
return res.redirect("/admin?cache=false");
}
let post;
try {
post = await blogPostDomain.getPost(id, dataloader);
} catch (err) {
return res.render(template, {
post: {},
pageId,
error: is.object(err.message) ? err.message : err
});
}
if (!post) {
return res.redirect("/404");
}
res.render(template, {
post,
pageId
});
}
};
};

Admin Router

Great! We have all our route handlers defined. Let create now the Express router that will connect the route paths to those handlers. But first a quick word about reading the form data.

Read multipart/form-data

I mentioned in the first part of this tutorial that I won’t get into details on how the views (the pug templates) are rendered. But let’s have a quick word about the form used to create or edit a blog post. If you look into the “views/admin/edit.pug” template file you will see that the form has its enctype format set to “multipart/form-data”. This format allows us to send files from the user browser along with the form input data. In order to parse the request data sent from the form inside Express, we will use a middleware on the route (the multer package).

With that said, create an admin.routes.ts file and add the following:

// modules/admin/admin.routes.tsimport express, { Router } from "express";
import multer from 'multer';
import { Context, Modules } from "./models";
import { AdminRoutesHandlers } from "./admin.routes-handlers";
const uploadInMemory = multer({
storage: multer.memoryStorage(),
limits: {
fileSize: 5 * 1024 * 1024 // no larger than 5mb
},
fileFilter: (req, file, cb) => {
// Validate image type
if (["image/jpeg", "image/png"].indexOf(file.mimetype) < 0) {
const err = new Error(
`File type not allowed: ${req.file.mimetype}`
);
return cb(err, false);
}
return cb(null, true);
}
});
export default (
_: Context,
routesHandlers: AdminRoutesHandlers,
{ images }: Modules
): Router => {
const router = express.Router();
router.get("/", routesHandlers.dashboard);
router.get("/create-post", routesHandlers.createPost);
router.get("/edit-post/:id", routesHandlers.editPost);
router.post(
"/create-post",
[uploadInMemory.single("image")], // middleware to parse form
routesHandlers.createPost
);
router.post(
"/edit-post/:id",
[uploadInMemory.single("image")], // middleware to parse form
routesHandlers.editPost
);
return router;
};

Not much to explain here. The dependency on the Images module is declared but not yet used (we will need it when we’ll upload featured images to Google Storage).

Let’s now export this router from our Admin module “index.ts” entry file. Replace the content of the file with:

// modules/admin/index.tsimport { Router } from "express";
import initRoutes from "./admin.routes";
import initRoutesHandlers from "./admin.routes-handlers";
import { Context, Modules } from "./models";export interface AdminModule {
webRoutes: Router;
}
export default (context: Context, { blog, images }: Modules) => {
const routesHandlers = initRoutesHandlers(context, { blog });
return {
webRouter: initRoutes(context, routesHandlers, { images })
};
};

The module requires two arguments to be provided (Context and Modules) when instantiating. Let’s add them in our “modules.ts” file:

// modules.ts
...
export default (context: Context): AppModules => {
const utils = initUtilsModule();
const images = initImagesModule();
const blog = initBlogModule(context, { utils, images });
const admin = initAdminModule(context, { blog, images }); // edit
...

Let’s now connect our admin router to the app routes under the /admin path.

// routes.ts...export default (
{ logger, config }: Context,
{
app,
modules: { blog, admin }
}: { app: Express; modules: AppModules }
) => {
/**
* Web Routes
*/
app.use("/blog", blog.webRouter);
app.use("/admin", admin.webRouter); // Add this line
...

Great! You should now be able to access the /admin route and from there create a new post. You should also be able to edit a post, but if you try to delete a post you’ll noticed that is not working. This is because the link calls an API endpoint (through an HTTP Request on the client) that we haven’t defined yet. Let’s do that right now!

Blog Module API

We are going to create a small REST API for our Blog module that will allow us to:

  • Delete a BlogPost
  • Read/Create & Delete comments (we’ll see that in a future post)

Open the “blog.routes.ts” file in our Blog module and make the following modifications:

// modules/blog/blog.routes.ts...webRouter.get("/:id", blogPost.routesHandlers.detailPost);// API
const apiRouter = express.Router();
apiRouter.delete(
"/blog-posts/:id",
blogPost.routesHandlers.deletePost
);
return {
webRouter,
apiRouter // Add this line
};

Let’s export the API router from our Blog module:

// modules/blog/index.ts...export interface BlogModule {
webRouter: Router;
apiRouter: Router; // Add this line
blogPost: BlogPostModule;
}
export default (context: Context, modules: Modules): BlogModule => {
const blogPost = initBlogPost(context, {});
const { webRouter, apiRouter } = initRoutes(context, {
blogPost
});
return {
webRouter,
apiRouter // Add this line
blogPost,
};
};

We now need to create the deletePost() handler to our BlogPost routes handlers. Open the “blog-post.routes-handlers.ts” file and add the following:

// modules/blog/blog-post/blog-post.routes-handlers.ts...export interface BlogPostRoutesHandlers {
listPosts(req: Request, res: Response): any;
detailPost(req: Request, res: Response): any;
deletePost(req: Request, res: Response): any; // Add this line
}
...async detailPost(req, res) { ... },async deletePost(req, res) {
let result: DeleteResult;
try {
result = await blogPostDomain.deletePost(req.params.id);
} catch (err) {
return res.status(err.status || 401).end(err.message);
}
if (!result.success) {
return res.status(400).json(result);
}
return res.json(result);
},
...

Nothing fancy. We simply call the deletePost() method on our domain layer and return the result.
Finally, we need to connect our brand new Blog API router to our main App routes.

// routes.ts.../**  
* Web Routes
*/
app.use("/blog", blog.webRouter);
app.use("/admin", admin.webRoutes);
/**
* API Routes
*/
const { apiBase } = config.common; // Add this line
app.use(apiBase, blog.apiRouter); // Add this line
...

Great! We can now create/edit and delete blog posts. And with that, we are done with the Admin module.

Thanks for reading, please reach out in the comments if something is not clear or if you have any question.

Our application is starting to take shape. The next feature we are going to add is the possibility to upload a featured image to Google Storage for our blog posts.

We will do that in the next section of the tutorial when building our Images module.

--

--