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 2builds
: we are specifying what builder (in our case@now/node
) we want to make use of for building our routesroutes
: 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:
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 a single note
GET /api/notes/:id
Update a note
PUT /api/notes/:id
Headers:
{
"Content-Type": "application/json"
}
Body:
{
"title": <string>,
"body": <string>
}
Delete a note
DELETE /api/notes/:id
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.