Server Architecture

Database

This uses Node.js MongoDB driver. This doesn't use mongoose to isolate the validation part for different databases.

Controllers

All the business logic is in server/controllers and API files only export these controllers with a handler called NextApiHandler. This gives extra flexibility to reuse controllers in Express when needed.

Models

models contains files with TypeScript interfaces and Joi Schemas. This part is reusable in both server and client thats why its at top level.

There is IDocument interface which has some common properties for every document. You can extend your rest of interfaces with this.

Middlewares

server/middleware you can create your own middlewares and wrap the controllers either before passing to NextApiHandler or while defining controller itself.

withAuth is a good example for basic syntax while writing a middleware.

import { createPost } from 'server/controllers/post.controller'
import { NextApiHandler } from 'server/middelwares/NextApiHandler'
import { withAuth } from 'server/middelwares/withAuth'
export default NextApiHandler({
// on post method
post: withAuth(createPost),
})

Data Access layer

There is server/dal. I'm not saying to compulsorily do it this way. But it can be helpful sometimes.

This files contains all the functions to interact with database. For example, find(), findOne().

Goal here is regardless of database driver this API should remain constant, so when we need to change database we only change this file and rest of the code will work just fine. But there are certainly some downsides to it over using the direct driver API which is more flexible we can say.

Error handling

NextApiHandler is a smart handler and can detect errors and send appropriate status codes with messages. First, see this code, and then the explanation below.

// manually thrown by us
if (e instanceof ApplicationError) {
res.status(e.code).send(e.message)
return
}
// joi schema validtaion error
if (e instanceof Joi.ValidationError) {
return res.status(400).json({
error: 'Validation Error',
message: e.message,
details: e.details,
})
}
// unknown error (your job to recoginize)
console.error(e)
res.status(500).send('Internal Server Error')

ApplicationError means its and one of our own custom errors. Custom errors are defined in this file server/errors.ts.

So anywhere from our application, we can just for example throw new NotFoundError() and it will be taken care of to send valid status code. You can also pass with custom messages as throw new UnauthorizedError('You need to login') or default these errors with some message.

While writing a error we would need to extend it with ApplicationError to make sure it gets recognized in error handler. For Example,

export class MethodNotAllowed extends ApplicationError {
constructor(message: string = 'Method not allowed', ...args: any) {
super(405, message, ...args)
}
}