Custom API server with basic CRUD — JS, Apollo, GraphQL & MongoDB
Tags: api, graphql, mongodb, javascript
Published: 2023-01-16 06:26:16

*[...] 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.*

![APIs, APIs everywhere](https://cdn-images-1.medium.com/max/800/1*qVibWDBXjSwIcBjO8C8zJA.png)

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:

```bash
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*](https://www.mongodb.com/docs/drivers/node/current/), 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*](https://www.linode.com/docs/guides/set-up-mongodb-on-docker/#before-you-begin)*. Alternatively, you can use* [*MongoDB Atlas*](https://www.mongodb.com/cloud/atlas/register) *(which is remote & ready solution).*

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

```bash
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](https://www.apollographql.com/docs/apollo-server/).

Initialization
--------------

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

```bash
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:

```bash
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`:

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

Apollo Server setup
-------------------

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

```bash
npm install @apollo/server graphql -S
```

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

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

Define temporary `movie` *GraphQL* schema for testing purposes below:

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

  type Query {
    movies: [Movie]
  }
`
```

And add `movies` data set below:

```javascript
...
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.

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

Let's define our *Apollo Server* instance:

```javascript
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:

```bash
npm start
```

Which should print back:

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

If you open [`http://localhost:4000/`](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:

```graphql
query Movies {
   movies {
      title
      director
   }
}
```

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

```json
{
   "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`:

```javascript
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):

```graphql
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:

```bash
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:

```javascript
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`:

```javascript
...
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/`](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:

```bash
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*:

```bash
npm i mongoose -S
```

Then import it in our `./index.js`:

```javascript
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*):

```javascript
...
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:

```javascript
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`.

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

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

```javascript
...
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`:

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

### Create (CRUD)

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

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

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

```javascript
...
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/`](http://localhost:4000/) by querying this mutation:

```graphql
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.

```json
{
  "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:

```graphql
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):

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

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

```javascript
...
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/`](http://localhost:4000/) using this query:

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

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

```json
{
  "id": "63c224272a9dd1ef31d73de5"
}
```

### Update (CRUD)

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

```graphql
...
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:

```javascript
...
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:

```graphql
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:

```json
{
  "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:

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

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

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

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

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

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

```json
{
  "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**](https://www.npmjs.com/package/jsonwebtoken) with basic key based permission logic;

-   Integrate this with a front-end framework, for example, [**Vue.js**](https://vuejs.org/guide/introduction.html);

-   *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*);