Simple CRUD API with Vercel's Now

Last Updated: February 14, 2020.

By the end of this note, you’ll know – in a step by step fashion – exactly how to build and deploy a fully functional REST API with CRUD abilities on Vercel’s serverless function platform.

Over the next few minutes, we would be building an API for notes handling. The API would have the following route definitions:

  • Create a new note: POST /notes

  • Fetch all notes: GET /notes

  • Fetch a single note: GET /notes/:id

  • Update a note: PUT /notes/:id

  • Delete a note: DELETE /note/:id

For simplicity, we would be storing all created notes in a data structure in memory, and not persisting to a proper datastore – we will explore that in a future article.

Again, please note that ideally, we would be making use of a proper datastore to persist created notes as the runtime for these kinds of services are often stateless – read more about this here.

Let’s get started 💪🏾

Project Setup

To follow along without any hiccups, you’d need the following:

  • a terminal environment (*nix shell environment: e.g bash, zsh, fish, e.t.c)
  • a recent Node installation (any version from v10 up works)
  • npm or yarn (for installing the now cli so we can run a dev server locally)
  • a text editor for code
  • a Vercel account – Click here to quickly set up one

Prerequisites out of the way, let’s get started building this API! 💪🏾

Create The Project Directory

mkdir notely-api && cd notely-api

Initialize a new node project

npm init -y

At this point, your project’s folder should have the following structure:

└── notely-api
    └── package.json

now, open the notely-api directory in your code editor of choice.

Finally, if you don’t have it already, install the now CLI globally on your machine …

npm install -g now

… and authenticate with the Vercel account you created earlier …

now login

NOTE: For this note, we’ll be making use of the now cli v17.0.3

Scaffold Project

Lastly, before heading off to building the endpoints proper, let’s get all the files we’ll be needing for the project created.

In the root directory of the project run the following:

mkdir controllers services routes db && touch controllers/note.controller.js services/note.service.js routes/note.route.js db/notes.json

At this point the project folder should look like this:

notely-api
├── controllers
│   └── note.controller.js
├── db
│   └── notes.json
├── package.json
├── routes
│   └── note.route.js
└── services
    └── note.service.js

Open up the file db/notes.json in your code editor, and add the following as well:

{
  "notes": [],
  "usedIds": []
}

The above is what we are going to be making use of as our makeshift database.

With most of the initial boring setup out of the way, let’s build out the endpoint for creating notes.

Create A Note

The general shape of this endpoint and the data which should be expected should be something as thus

POST /api/notes

Headers:
{
  "Content-Type": "application/json"
}

Body:
{
  "title": <string>,
  "body": <string>
}

We’ll kick off by creating the note creation service. Inside of services/note.services.js put the following:

function saveNote({ title, body }) {
  const id = getNextId();
  const previousNotes = fetchAllNotes();
  const newNote = { id, title, body };
  const updatedNotes = [...previousNotes, newNote];
  writeNotesToDB(updatedNotes);
  updateIdTracker();

  return newNote;
}

const NoteService = { saveNote };

module.exports = NoteService;

// ========== Service Utilities ========== //

const db = require('../db/notes.json');

function fetchAllNotes() {
  return db.notes;
}

function writeNotesToDB(notes) {
  db.notes = notes;
}

function getLastId() {
  const [lastId = 0] = db.usedIds.slice(-1);
  return lastId;
}

function getNextId() {
  const lastId = getLastId();
  return lastId + 1;
}

function updateIdTracker() {
  const nextId = getNextId();
  db.usedIds.push(nextId);
}

What the service does is pretty self-explanatory from the code above, so no need to dwell. Next up, let’s create the controller; inside of controllers/note.controller.js put the following:

import NoteService from '../services/note.service';

function createNote(req, res) {
  const { title, body } = req.body;
  const createdNote = NoteService.saveNote({ title, body });
  res.status(200).json({
    success: true,
    message: 'Note created successfully',
    data: createdNote,
  });
}

const NoteController = { createNote };

module.exports = NoteController;

Lastly, inside of routes/note.route.js, put the following:

const NoteController = require('../controllers/note.controller');

function requestHander(req, res) {
  const { method } = req;

  switch (method) {
    case 'POST':
      const newNote = NoteController.createNote(req, res);
      return newNote;
  }
}

module.exports = requestHander;

A final piece before we can move on to test the endpoint is creating a now.json file in the project’s root directory. The now.json file is how we “instruct” the now platform on what requirements our project might have, the routes, and a lot more.

I’ll be going in-depth as to most of the configuration options present in future notes, but for now, you can hit their doc site to get the general gist.

For our API, the following config would suffice:

{
  "version": 2,
  "builds": [{ "src": "routes/*.js", "use": "@now/node" }],
  "routes": [{ "src": "/api/notes(.*)", "dest": "routes/note.route.js" }]
}

What’s going on here is,

  • version: we are specifying we want to make use of the now platform version 2
  • builds: we are specifying what builder (in our case @now/node) we want to make use of for building our routes
  • routes: we are specifying what endpoints our API would have, and the route handler file which we want to make use of for the endpoint

With the above config placed in a now.json file you’ll create in the project’s root directory, run the following command …

now

… and provide the required information to sync your Vercel account with the project. Once that’s done, you can run …

now dev

… to start a dev server we can test from.

With the server up and running, fire up an API testing client (or use cURL!) to test the endpoint to create notes:

create a note

Get Notes, Update Note, Delete Note

The process for setting up the other endpoints is fairly similar to that described above. So in a bid not to extend this article much further, outlined below are the final states for all the files:

services/note.service.js

function saveNote({ title, body }) {
  const id = getNextId();
  const previousNotes = fetchAllNotes();
  const newNote = { id, title, body };
  const updatedNotes = [...previousNotes, newNote];
  writeNotesToDB(updatedNotes);
  updateIdTracker();

  return newNote;
}

function getNotes() {
  const notes = fetchAllNotes();
  return notes;
}

function getNoteById(id) {
  const allNotes = fetchAllNotes();
  const [note] = allNotes.filter(note => note.id == id);
  return note;
}

function updateNote({ id, title = null, body = null }) {
  const allNotes = fetchAllNotes();
  const [noteToBeUpdated] = allNotes.filter(note => note.id == id);
  const remNotes = allNotes.filter(note => note.id != id);
  const updatedTitle = title || noteToBeUpdated.title;
  const updatedBody = body || noteToBeUpdated.body;

  const updatedNote = {
    ...noteToBeUpdated,
    title: updatedTitle,
    body: updatedBody,
  };

  const updatedNotes = [...remNotes, updatedNote].sort((a, b) => a.id - b.id);
  writeNotesToDB(updatedNotes);

  return updatedNote;
}

function deleteNote(id) {
  const allNotes = fetchAllNotes();
  const remNotes = allNotes.filter(note => note.id != id);
  writeNotesToDB(remNotes);
  return id;
}

const NoteService = {
  saveNote,
  getNotes,
  getNoteById,
  updateNote,
  deleteNote,
};

module.exports = NoteService;

// ========== Service Utilities ========== //

const db = require('../db/notes.json');

function fetchAllNotes() {
  return db.notes;
}

function writeNotesToDB(notes) {
  db.notes = notes;
}

function getLastId() {
  const [lastId = 0] = db.usedIds.slice(-1);
  return lastId;
}

function getNextId() {
  const lastId = getLastId();
  return lastId + 1;
}

function updateIdTracker() {
  const nextId = getNextId();
  db.usedIds.push(nextId);
}

controllers/note.controller.js

const NoteService = require('../services/note.service');

function createNote(req, res) {
  const { title, body } = req.body;
  const createdNote = NoteService.saveNote({ title, body });
  res.status(200).json({
    success: true,
    message: 'Note created successfully',
    data: createdNote,
  });
}

function getNotes(req, res) {
  const notes = NoteService.getNotes();
  res.status(200).json({
    success: true,
    message: 'Notes fetched successfully',
    data: notes,
  });
}

function getSingleNote(req, res) {
  const { id } = req.params;
  const note = NoteService.getNoteById(id);
  if (!note) {
    res.status(404).json({
      success: false,
      message: 'Note not found',
      data: null,
    });
  } else {
    res.status(200).json({
      success: true,
      message: 'Note fetched successfully',
      data: note,
    });
  }
}

function updateNote(req, res) {
  const { id } = req.params;
  const { title, body } = req.body;
  const note = NoteService.getNoteById(id);

  if (!note) {
    return res.status(404).json({
      success: false,
      message: 'Note not found',
      data: null,
    });
  }

  const updatedNote = NoteService.updateNote({ id, title, body });
  return res.status(200).json({
    success: true,
    message: 'Note updated successfully',
    data: updatedNote,
  });
}

function deleteNote(req, res) {
  const { id } = req.params;
  const note = NoteService.getNoteById(id);

  if (!note) {
    return res.status(404).json({
      success: false,
      message: 'Note not found',
      data: null,
    });
  }

  NoteService.deleteNote(id);
  return res.status(200).json({
    success: true,
    message: 'Note deleted successfully',
    data: null,
  });
}

const NoteController = {
  createNote,
  getNotes,
  getSingleNote,
  updateNote,
  deleteNote,
};

module.exports = NoteController;

routes/note.route.js

const NoteController = require('../controllers/note.controller');

function requestHander(req, res) {
  const { method } = req;

  switch (method) {
    case 'POST':
      const newNote = NoteController.createNote(req, res);
      return newNote;
    case 'GET':
      const response = handleGET(req, res);
      return response;
    case 'PUT':
      if (!singleNotePath.test(req.url)) return handleInvalid(req, res);
      req.params = { id: req.url.slice(baseURLPath.length) };
      return NoteController.updateNote(req, res);
    case 'DELETE':
      if (!singleNotePath.test(req.url)) return handleInvalid(req, res);
      req.params = { id: req.url.slice(baseURLPath.length) };
      return NoteController.deleteNote(req, res);
    default:
      return handleInvalid(req, res);
  }
}

module.exports = requestHander;

// ========== Utilities ========== //

// matches /api/notes/<id>
var singleNotePath = new RegExp(/^\/api\/notes\/\w{1,}\/?$/);
// matches /api/notes
var allNotesPath = new RegExp(/^\/api\/notes\/?$/);
var baseURLPath = '/api/notes/';

function handleGET(req, res) {
  const { url } = req;

  if (allNotesPath.test(url)) {
    return NoteController.getNotes(req, res);
  } else if (singleNotePath.test(url)) {
    const id = url.slice(baseURLPath.length);
    req.params = { id };
    return NoteController.getSingleNote(req, res);
  }
}

function handleInvalid(req, res) {
  return res.status(404).json({
    success: false,
    message: 'Invalid route',
    data: null,
  });
}

Testing the Endpoints

with all those in place, we can move on to testing the remaining endpoints.

Get all notes

GET /api/notes

get all notes

Get a single note

GET /api/notes/:id

get single note

Update a note

PUT /api/notes/:id

Headers:
{
  "Content-Type": "application/json"
}

Body:
{
    "title": <string>,
    "body": <string>
}

update a note

Delete a note

DELETE /api/notes/:id

delete a note

Deploying

To deploy the api and get a live URL you can test with, simply run …

now --prod

… and follow the prompts. Once completed, you’ll get your live url 🎉

Summing Up

This is by no means the only way APIs can be or should be built using the Vercel platform, it’s just one way I’ve explored, borrowing ideas from working with [in my opinion] the elegant expressjs.

The code for this note is available on github. Feel free to fork and extend or raise issues for areas that can be improved.