Next.js Backend

Photo by Taylor Vick on Unsplash

Next.js Backend

Api, middlewares, ssr, etc.

ยท

5 min read

I started making an app called OpenJira, which is kinda like a clone of Jira Software. If you don't know about it, it is an app for following tasks and helps when working with a team and stuff.

You can drag and drop entries between different columns depending on the status of the task. It's still in its early stages but you get the point.

I am not very interested in Jira but the purpose of the app was to learn more about setting up endpoints with Next.
In the project, I am using the react context API to manage the main state of the application. And using my backend(MongoDB) to hydrate the state with entries.

Backend with Next

Developing your backend with next takes a lot less code than if you wanted to make it with Node. Plus a lot of stuff is very similar so if you learned how to make a backend with node, transiting to Next shouldn't be too complicated. Let's see how to make a simple backend! (my method anyway).

Making a Seed

Create a folder named database or db.

In db.ts you will have the connect() and disconnect() functions, these do what you expect them to do and will be called every time we make HTTP requests.

import mongoose from 'mongoose';
mongoose.set("strictQuery", false);

/** 
 * 0 = disconected
 * 1 = connected
 * 2 = connecting
 * 3 = disconnecting
 */

const mongoConnection = {
    isConnected: 0
}

export const connect = async() => {
    if (mongoConnection.isConnected === 1){
        console.log('Already connected');
        return;
    }

    if (mongoose.connections.length > 0){
        mongoConnection.isConnected = mongoose.connections[0].readyState;

        if (mongoConnection.isConnected === 1){
            console.log('Using prev connection');
        }

        await mongoose.disconnect();
    }

    await mongoose.connect(process.env.MONGO_URL || '');
    mongoConnection.isConnected = 1;
    console.log('Connected to MongoDB', process.env.MONGO_URL);

}

export const disconnect = async() => {

    if (process.env.NODE_ENV === 'development') return;

    if (mongoConnection.isConnected !== 0) return;
    await mongoose.disconnect();
    console.log('Disconnected to MongoDB');
}

The mongoose.connect() the function can take a URL as an argument and depending on what happens will return a number. So basically what we are doing is determining what to do and what to output in the console whenever we connect to the database.

Models

My favorite part about backend development. Here we specify the Schemas of our models in the database or the structure of the data that we want to store.

import mongoose, {Model, Schema} from "mongoose"; 
import { Entry } from "../interfaces";

export interface IEntry extends Entry{
    //Extra properties if wanted
}

const entrySchema = new Schema({
    description: {
        type: String, 
        required: true
    },
    createdAt: {
        type: Number
    },
    status: {
        type: String,
        enum: {
            values: ['pending', 'in-progress', 'finished'],
            message: '{VALUE} not a valid state'
        },
        default: 'pending'
    }
});

const EntryModel: Model<IEntry> = mongoose.models.Entry || mongoose.model('Entry', entrySchema);

export default EntryModel;

The nitty-gritty

Inside the API directory, we define the paths to our endpoints the same way we define the paths to our pages (it's brilliant). In the entries directory. I have defined 2 paths, the index (which is just the default one) and the [id] path.

Outside of it, there is the bad-request an endpoint created just so that if there is an error or bad request, it will redirect the request to that endpoint.

Index

In this path, I defined the getEntries() and createEntry() functions, they are the most basic ones and don't require special information.

import type { NextApiRequest, NextApiResponse } from 'next'
import { db } from '../../../database';
import { Entry, IEntry } from '../../../models';

type Data =
    | {message: string }
    | IEntry[]
    | IEntry

export default function handler(req: NextApiRequest, res: NextApiResponse<Data>) {

    switch (req.method) {
        case 'GET':
            return getEntries( res );

        case 'POST':
            return createEntry(req, res);

        default:
            res.status(400).json({ message: 'Endpoint does not exist' })
    }


}

const getEntries = async ( res: NextApiResponse<Data> ) => {

    await db.connect();
    const entries = await Entry.find().sort({createdAt: 'ascending'})
    await db.disconnect();

    res.status(200).json(entries);
}

const createEntry = async ( req:NextApiRequest , res: NextApiResponse<Data> ) => {
    const { description = ''} = req.body;
    const newEntry = new Entry({
        description,
        createdAt: Date.now(),
    });

    try {
        await db.connect();
        await newEntry.save();
        await db.disconnect();

        return res.status(200).json(newEntry);

    } catch (error) {

        await db.disconnect();
        console.log(error);
        return res.status(500).json({
            message: 'Something went wrong'
        })
    }
}

If you are working with TypeScript, you should define a type at the beginning of the file that defines the type of data that these functions will output. We also have a handler() function which depending on the request method received will return one of the 2 functions.

[id]

I love this one, whenever you want to make a function that needs a certain id or something that you want to send on the URL path, you can make it in here.

Here's an example of a function that gets a certain entry by the id passed in with the url.

const getEntry = async (req: NextApiRequest, res: NextApiResponse<Data>) => {
    const { id } = req.query;
    await db.connect();

    const entry = await Entry.findById(id);

    if (!entry) {
        res.status(400).json({ message: 'Entry non-existent with id: ' + id });
    }

    try {
        await db.disconnect();
        res.status(200).json(entry!);

    } catch (error: any) {
        console.log(error);
        await db.disconnect()
        res.status(400).json({message: error.errors.status.message})
    }  

}

Finishing up...

That's a lot of information and I hope you found it useful. That's all I can say for now. In the future, I might post about SSR, since I need to generate pages from the server for every entry on the database. See ya later!!๐Ÿ˜๐Ÿ˜

ย