Custom API server with basic CRUD — JS, Apollo, GraphQL & MongoDB

Custom API server with basic CRUD — JS, Apollo, GraphQL & MongoDB

·

10 min read

[…] All that was left to do for my cross-platform hybrid web app was to test it with actual content and create CRUD. First thought, obviously, was to write middleware for back-end that will communicate with database. However, I had planned on adding more features over time, so doing that on multiple platforms would be overkill. Therefore, I quickly decided on building API server, that could... well... do it all.

The most commonly used data format for transferring data to a client is JSON and for controlling it, well.. used to be REST (don’t throw rocks at me just yet 😅). If you gradually increase your app’s complexity as well as database, it can and will be a very resource-heavy solution in long term. Luckily for us, there are 2 much better alternatives — GraphQL and gRPC. Even better — there is also Node.js friendly Apollo Server (GraphQL server).

So, in this article “slash” tutorial, I’ll try to dig into creating a custom API Apollo Server and as a bonus, we’ll write some basic CRUD for it.

Let’s dig in! 👏

Before you jump in

It goes without saying, that you’ll need basic knowledge in Node.js, NPM and how to use its command-line tool. At the moment of writing this tutorial, I had the following versions set-up:

node -v
v16.16.0
npm -v
8.19.2

What about the database? I personally prefer going with good old MongoDB. If you have avoided it until now, get to know it better here, it’s intuitive and fast. Oh, and we’ll also use it the OOP way**,** so meet Mongoose it will be our “driver” for MongoDB.

We’ll be running our MongoDB server on Docker. Read —how to install MongoDB on Docker. Alternatively, you can use MongoDB Atlas (which is remote & ready solution).

I installed and initialized MongoDB within Docker like so (replace mongoadmin and mongopasswd to whatever you want):

docker pull mongo
docker run -d  --name mongodb  -p 27017:27017 -e MONGO_INITDB_ROOT_USERNAME=mongoadmin -e MONGO_INITDB_ROOT_PASSWORD=mongopasswd mongo

Lastly, instead of writing our API core ourselves, we’ll be using the star of this episode — Apollo Server (a.k.a. GraphQL server). It has detailed documentation available here.

Initialization

Let’s dig in and start by creating our project folder:

mkdir mag-api-server
cd mag-api-server

We’ll go with MAG as in M*ongoDB, **A*pollo and *G**raphQL for the sake of... me loving abbreviations.* 🤷‍♂️

Then initialize our Node.js project:

npm init -y

Let’s update our package.json by setting type to module (so we can load our JS as ES modules) and changing npm test command to npm start:

...
"type": "module",
"scripts": {
   "start": "node index.js"
},
...

Apollo Server setup

Our project directory is set up, so let’s install Apollo Server with GraphQL:

npm install @apollo/server graphql -S

Then create an index.js file in your project root folder and import packages from above in it.

import { ApolloServer } from "@apollo/server"
import { startStandaloneServer } from "@apollo/server/standalone"

Define temporary movie GraphQL schema for testing purposes below:

...
const typeDefs = `#graphql
  type Movie {
    title: String
    director: String
  }

  type Query {
    movies: [Movie]
  }
`

And add movies data set below:

...
const movies = [
   {
      title: "Edward Scissorhands",
      director: "Tim Burton",
   },
   {
      title: "The Terrifier 2",
      director: "Damien Leone",
   },
]

Next, we’ll need to define a resolver.

⚡”Resolver tells Apollo Server how to fetch the data associated with a particular type.” Because our movies array is hard-coded, the corresponding resolver is straightforward.

...
const resolvers = {
   Query: {
      movies: () => movies,
   },
}

Let’s define our Apollo Server instance:

const server = new ApolloServer({
   typeDefs,
   resolvers,
})

const { url } = await startStandaloneServer(server, {
   listen: { port: 4000 },
})

console.log(`🚀  Server ready at: ${url}`)

And test run our server:

npm start

Which should print back:

> mage-api-server@1.0.0 start
> node index.js
🚀  Server ready at: http://localhost:4000/

If you open http://localhost:4000/, you will see a sandbox environment, where we will be executing GraphQL queries.

Go ahead and run this query in the operations tab:

query Movies {
   movies {
      title
      director
   }
}

If everything is set up correctly, you will receive this JSON response:

{
   "data": {
      "movies": [
         {
            "title": "Edward Scissorhands",
            "director": "Tim Burton"
         },
         {
            "title": "The Terrifier 2",
            "director": "Damien Leone"
         }
      ]
   }
}

Restructure Schemas & resolvers

With our Apollo Server running and querying, let’s restructure our project files and create more automated type schema inclusion. We’ll start by creating ./schemas/ folder which will hold our GraphQL type schemas.

For resolvers — create ./resolvers/ folder as well as ./resolvers/movie.js file. Next, cut our movies constant data set and resolvers definitions from index.js and paste them into ./resolvers/movie.js. Finally, prefix resolvers with export and rename it to moviesResolvers:

const movies = [
   {
      title: "Edward Scissorhands",
      director: "Tim Burton",
   },
   {
      title: "The Terrifier 2",
      director: "Damien Leone",
   },
]

export const moviesResolvers = {
   Query: {
      movies: () => movies,
   },
}

Next, create ./schemas/Movie.graphql file, cut Movie type schema from ./index.js and paste it in our newly made file (without GraphQL syntax and JS definition):

type Movie {
   title: String
   director: String
}

type Query {
   movies: [Movie]
}

Before we create a loader for resolvers and schemas, let’s install the necessary GraphQL Tools:

npm i @graphql-tools/load @graphql-tools/schema @graphql-tools/graphql-file-loader -S

Now create ./loader.js in the project root folder and import both scheme and resolvers using our new tools:

import { loadSchema } from "@graphql-tools/load"
import { GraphQLFileLoader } from "@graphql-tools/graphql-file-loader"
import { moviesResolvers } from "./resolvers/movie.js"

export const typeDefs = await loadSchema("./schemas/**/*.graphql", { loaders: [new GraphQLFileLoader()] })
export const resolvers = [moviesResolvers]

⚡Using loaders in the manner above will not only automize module, resolver and schema import but also make code more readable and writable in long term.

Let’s go back to ./index.js and import schemas and resolvers:

...
import { resolvers, typeDefs } from "./loader.js"
...

To test if everything works fine, re-run the server npm start command and query movies in http://localhost:4000/.

MongoDB via Mongoose

We got our type query-able schema and resolvers. Let’s remove the hard-coded data set in ./resolvers/movie.js and connect the database instead.

Adding MongoDB should be pretty intuitive, however thinking ahead we’d probably want our data the OOP way, right? This is where Mongoose comes in. It’s a great “driver” for that.

As mentioned in “Before you jump in” section, we will be using Dockerized MongoDB approach. To test if our MongoDB container is running, let’s run this command:

docker ps -a

If you see your container and its status indicates it’s running, we’re ready to continue.

So let’s continue by installing Mongoose:

npm i mongoose -S

Then import it in our ./index.js:

import mongoose from "mongoose"

Now, initialize our MongoDB connection somewhere above our server constant (replace mongoadmin and mongopasswd to whatever you provided when initializing Docker container):

...
mongoose.Promise = global.Promise
mongoose.set("strictQuery", false)
mongoose.connect("mongodb://mongoadmin:mongopasswd@localhost:27017/?authSource=admin")
......

So, we have our connection to MongoDB, but we still need to replace hard-coded data set with a model.

Create ./models folder and create ./models/movie.js. Open it up and create movie database schema:

import mongoose from "mongoose"

const Schema = mongoose.Schema
const MovieSchema = new Schema(
   {
      title: {
         type: String,
         default: "",
         required: true,
      },
      director: {
         type: String,
         default: "",
         required: true,
      },
   },
   {
      timestamps: {
         createdAt: "created_at",
         updatedAt: "updated_at",
      },
   }
)

export default mongoose.model("movie", MovieSchema)

In theory —  this is it. However, I mentioned something about CRUD before, so let’s dig into that and customize our MAG API a little bit more. 😅

CRUD

Because I love writing so much, I want us to create simple CRUD logic in our only resolver ./resolvers/movie.js. If you haven’t already, remove hard-coded dummy data set and import ./models/movie.js.

import Movie from "../models/movie.js"
...

Also, let’s redefine our movies query in ./resolvers/movie.js :

...
export const moviesResolvers = {
  Query: {
    async movies(root, {}, ctx) {
      return await Movie.find()
    },
  },
}
...

Lastly, before we continue — it is always smart to use some kind of unique identifiers for your records, therefore let’s go ahead and use MongoDB default one _id and reuse it in our movie schema ./schemas/Movie.graphql:

type Movie {
  _id: ID!
  title: String
  director: String
}
...

Create (CRUD)

Define CREATE mutation in our ./schemas/movie.graphql type schema:

...
type Mutation {
   addMovie(title: String!, director: String!): Movie!
}

Next, CREATE mutation resolver below in ./resolvers/movie.js:

...
export const moviesResolvers = {
  ...
  Mutation: {
     async addMovie(root, { title, director }, ctx) {
        return await Movie.create({
           title,
           director,
        })
     },
  }
}

Now, let’s restart our server and test if we can add new movie via sandbox at http://localhost:4000/ by querying this mutation:

mutation Mutation($director: String!, $title: String!) {
  addMovie(director: $director, title: $title) {
    _id
    title
    director
  }
}

And for variables let’s enter data from a previously deleted data set. Write couple of different ones for the diversity of things.

{
  "title": "Prometheus",
  "director": "Ridley Scott"
}

When you’re ready —  run the mutation query.

To confirm that we actually saved the movie, open up a new query tab in sandbox and re-run:

query Query {
  movies {
    _id
    title
    director
  }
}

You will receive your newly created movie within JSON response.

Now, we have to mirror our steps above and create read, update and delete logic. There is no CRUD without RUD. 🥁

Read (CRUD)

We already have a method to READ all movies, so let’s just define READ query in our ./schemas/movie.graphql type schema for reading a single movie (using its ID):

...
type Query {
  ...
  getMovie(_id: ID!): Movie
}

Define READ query resolver below in ./resolvers/movie.js:

...
Query: {
  ...
  async getMovie(root, { _id }, ctx) {
    return await Movie.findOne({ _id })
  }
}
...

Restart the server and test if you can get a movie by its _id via sandbox at http://localhost:4000/ using this query:

query Query($id: ID!) {
  getMovie(_id: $id) {
    _id
    director
    title
  }
}

And use your newly added movie’s _id as a variable value:

{
  "id": "63c224272a9dd1ef31d73de5"
}

Update (CRUD)

Define UPDATE mutation in our ./schemas/movie.graphql type schema:

...
type Mutation {
  ...
  updateMovie(_id: ID!, title: String, director: String): Movie
}

Now, to update the movie, we’ll UPDATE mutation in out ./resolvers/movie.js like so:

...
Mutation: {
  ...
  async updateMovie(root, { _id, title, director }, ctx) {
    return await Movie.findOneAndUpdate({ _id }, { title, director })
  },
}

Restart the server, define the query and provide _id and one or both variables for it to update:

mutation Mutation($id: ID!, $title: String, $director: String) {
  updateMovie(_id: $id, title: $title, director: $director) {
    director
    title
  }
}

I’m updating “Prometheus” movie title and director, so I’m providing its _id value from the previous query response and the rest below it:

{
  "id": "63c224272a9dd1ef31d73de5",
  "title": "Antichrist",
  "director": "Lars von Trier"
}

Delete (CRUD)

Time flies and taste in movies changes, so we will obviously need a way to delete a movie in future, therefore let’s go and define DELETE mutation in our ./schemas/movie.graphql type schema:

...
type Mutation {
 ...
 deleteMovie(_id: ID!): Movie
}

Next, define DELETE mutation resolver below in ./resolvers/movie.js:

Mutation: {
  ...
  async deleteMovie(root, { _id }, ctx) {
    return await Movie.findOneAndDelete({ _id })
  },
 },

Finally, let’s go ahead, restart and test with this query:

mutation Mutation($id: ID!) {
  deleteMovie(_id: $id) {
    _id
    director
    title
  }
}

Now let’s define _id of movie that we’ll be deleting:

{
  "id": "63c224272a9dd1ef31d73de5"
}

Finally, restart the server and query all movies to see if it all worked!

🎉Congratulations!

You did it! You read this lengthy “how-to” tutorial and created your own API server with basic CRUD. 👏

Leave a comment below if and where you had trouble running the server, or if you simply want to make a suggestion.


What’s next?

This API server is bare-bone by itself, so your journey doesn’t end here. “One simply does not CRUD without an auth”. Surely you wouldn’t want to lose all your precious po.. I mean movies!? 👀

So, here are some ideas on what to do next:

  • What is API without a key? Try integrating JWT with basic key based permission logic;

  • Integrate this with a front-end framework, for example, Vue.js;

  • Containerize your API server and MongoDB for Docker (make an easily deployable container);

  • Write sanitizers & conditions to work with duplicates or queries returning errors on non-existing records;

  • Secure queries and mutations by writing checks for inputs and customizing callbacks (I might do part 2 regarding this);

Did you find this article valuable?

Support richardev by becoming a sponsor. Any amount is appreciated!