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 usif (e instanceof ApplicationError) { res.status(e.code).send(e.message) return}// joi schema validtaion errorif (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) }}