Building a modern Web application can be a complex task - it's likely that if you're looking to build something you'll start thinking about architecture, and how to separate your concerns, and that there are many ways that can be done. Traditionally speaking, this means having a separate frontend and backend, with separate codebases, maybe using different languages, frameworks, or technologies in order to get the job done.
In this article, we'll look at an alternative using something wonderful from the world of frontend development: Next.js. We'll look at how a monolithic architecture can be convenient and powerful, without necessarily suffering from the tight coupling usually linked with this approach. But first, a quick primer on how Next.js works.
How Next Works
If you're new to this tech, Next.js is a Jamstack framework, designed to solve some of the problems associated with building traditional Single Page Apps (SPAs). Not only does it support Server-Side Rendering, but also Static Site Generation, allowing developers to build apps with some static pages (think marketing / about pages!) and some dynamic pages that load super quick and are SEO-friendly. Depending on your use case, you can usually get Next.js to suit your needs, with very few caveats.
To put it briefly, this works because when you "run" a Next.js app, you're actually running a Node.js server (though if you're building a completely static site, you can just export all the static files and host them wherever, but we won't be talking about that approach here). As a result, your app is 'alive', in contrast with e.g. a Create React App, where a JS bundle is hosted somewhere and downloaded to the client where the whole app is rendered only on the client side.
As a result of this, it's possible to define some code that only ever runs on the server, that the client will never see. It's starting to look like given that we have server-side-only code, we could maybe start doing some stuff that we couldn't (more like shouldn't) do before on the client, e.g. talk to a database directly, perform operations that require several separate calls, etc. Isn't this starting to sound like an HTTP API?
Next.js supports this concept as a first-class citizen in its architecture. Routing is opinionated, and uses the filesystem to determine page routes in your app, using files and folders in a directory called pages to establish this - e.g. if you create a file in
mine.tsx you'd be able to find the page in your browser at
<your-domain>/todos/mine . Anything under the path
pages/api will only run on the server. There we have it! What we refer to as Next API Routes. This now means we can build a full-stack application with a single codebase, a "monolithic" architecture. Right off the bat we get several nice-to-haves for free:
- Single build process & easy-to-run dev environment for whole app (just run
- Simpler deployments (
next startin any node-compatible environment)
- No CORS worries by default, the frontend and backend are on the same origin!
- Monorepo-like sharing by default (think same
eslint) without having to use monorepo managers like
More experienced developers may see an approach like this as a step backwards, but it's arguable more like a step towards better consolidation of code & resources. Logical concerns can be separated without physical separation itself, and a monolithic architecture is beginning to make more sense for Web apps, most notably with the rise of Incremental Static Regeneration and Server Side Rendering.
As listed in the documentation, building API routes with Next looks a lot like building a basic API with Node.js:
Nice and simple. You can use this approach to very quickly and simply build out some server-side functionality!
But what if we wanted to start building a REST API, differentiating between different HTTP methods on the same endpoint, modelling our API in terms of resources? What if we wanted to start composing behavior into our API endpoints?
Our example quickly swells to something like this:
And that's probably the best case, as we've been able to factor out all the actual business logic into the functions
doAThirdThing. It'd likely get worse once we'd start getting request bodies etc. Not terrible, but we can likely make this a little easier to read, and a little easier to maintain.
Often, developers opt for a framework when building out APIs, as they offer alternative means of representing resources, and many tools and utilities designed to make this process easier. One such example is NestJS (Not to be confused with Next.js!). This is a batteries-included, powerful "framework for building efficient, scalable Node.js server-side applications". Heavily inspired by Angular, this framework allows developers to use abstractions to represent API routes, including reusable services, controllers, modules and more.
Here is a simple example copied from its documentation:
As we can see, this approach uses OOP ideas and decorators to achieve the goals described previously. We specify only the HTTP methods we intend to make available as methods on a class, instead of control flow paths in a function. A little more declarative, and a great foundation if we're looking build out a REST API.
So how do we make use of this approach for Next.JS API Routes?
This library, written by the wonderful folks at Story of AMS, an eCommerce tech agency based in Amsterdam, decided to develop a solution using these concepts to make building an API easier to scale with Next.js. Let's revisit our earlier complex example rewritten using their library to demonstrate how it works:
As we can see, we have changed the way we express our resource handler, swapping a function for a class. On top of this, methods of the class are annotated with decorators to express their purpose.
Now, it's up to you which you prefer, as it's not as if this approach makes it much more concise. But it does, as mentioned earlier, transform our approach to describing our resources from an imperative one to a declarative one. I have found while building Next.js APIs out that this approach is a good deal easier to maintain, as eventually each route and each handler can and will get more complex as your application grows.
The last thing I wanted to look at from this example is the
requiresAuth decorator we see on some of the methods in this handler now. I wrote something like this custom handler for a project recently, and it's proven very useful. This is done using the
createMiddlewareDecorator utility provided by
Pretty strong stuff 💪
This approach is very flexible and can be applied to both classes and their methods to fit a variety of use cases and declutter your actual business logic.
The example I've shown here is very simple and just to give you an idea of the a practical monolithic approach to building a full-stack application using Next.js and its API routes, and how it can be made scalable server-side using
Thanks for reading!