Basics of Building a CRUD API with Node (no framework)

In this tutorial we once again create a full CRUD api without a database. In this article we won’t use a pre-existing framework but the standard node libraries that underpin all our favorite frameworks.

Summary of RESTful Convention

THe restful convention gives us a blueprint of making the basic routes for CRUD (Create, Read, Update, Delete) functionality in a uniform way.

API Restful Routes

Name of Route
Request Method
Endpoint
Result

Index
GET
/model
returns list of all items

Show
GET
/model/:id
returns item with matching id

Create
Post
/model
creates a new item, returns item or confirmation

Update
Put/Patch
/model/:id
Updated item with matching ID

Destroy
Delete
/model/:id
Deletes item with matching ID

If we weren’t build an API but instead rendering pages on the server there would be two additional routes. New, which renders a page with a form to create a new object, submitting the form triggers the create route. Edit, which renders a page with a form to edit an existing object, submitting the form triggers the Update route.

Since we are building an api, Edit and New aren’t necessary as the burden of collecting the information to submit to the Create and Update route will be on whoever builds the applications that consume the API. (Frontend Applications built in frameworks)

Building an API

Setup

  • Must have nodeJS installed

  • create an empty folder and navigate terminal into it

  • create a server.js file and create an npm project touch server.js && npm init -y

Since we are using the standard library there is no need to install any other libraries. There are two libraries to be aware of, “http” and “https”. They are pretty much the same but you use the latter for handling https connections (the things our frameworks figure out for us).

So to start our server…

server.js

// Import http library

const

http

=

require

(

"

http

"

)

// use env variable to define port with default

const

PORT

=

process

.

env

.

PORT

||

4000

//create our server object

const

server

=

http

.

createServer

()

// get the server to start listening

server

.

listen

(

PORT

,

err

=>

{

// error checking

err

?

console

.

error

(

err

)

:

console

.

log

(

`listening on port

${

PORT

}

`

)

})

Enter fullscreen mode

Exit fullscreen mode

If you run the server (node server.js) and go to localhost:4000 it just hangs cause we have no instructions built into our server to handle the incoming request. Essentially our server will pass the request details to a function for every request. So the next step is to create the function that will handle EVERY request.

There is two approaches:

The Event Based Approach

// Import http library

const

http

=

require

(

"

http

"

)

// use env variable to define port with default

const

PORT

=

process

.

env

.

PORT

||

4000

//create our server object

const

server

=

http

.

createServer

()

// We define a function that runs in response to the request event

server

.

on

(

"

request

"

,

(

request

,

response

)

=>

{

// handle request based on method then URL

switch

(

request

.

method

)

{

case

"

GET

"

:

switch

(

request

.

url

)

{

// response for unexpected get requests

default

:

response

.

statusCode

=

400

response

.

write

(

`CANNOT GET

${

request

.

url

}

`

)

response

.

end

()

}

break

case

"

POST

"

:

break

case

"

PUT

"

:

break

case

"

DELETE

"

:

break

default

:

// Send response for requests with no other response

response

.

statusCode

=

400

response

.

write

(

"

No Response

"

)

response

.

end

()

}

})

// get the server to start listening

server

.

listen

(

PORT

,

err

=>

{

// error checking

err

?

console

.

error

(

err

)

:

console

.

log

(

`listening on port

${

PORT

}

`

)

})

Enter fullscreen mode

Exit fullscreen mode

The Callback Approach

You could also pass this function as a callback to the createServer function.

// Import http library

const

http

=

require

(

"

http

"

)

// use env variable to define port with default

const

PORT

=

process

.

env

.

PORT

||

4000

//create our server object, pass server function as callback argument

const

server

=

http

.

createServer

((

request

,

response

)

=>

{

// handle request based on method then URL

switch

(

request

.

method

)

{

case

"

GET

"

:

switch

(

request

.

url

)

{

// response for unexpected get requests

default

:

response

.

statusCode

=

400

response

.

write

(

`CANNOT GET

${

request

.

url

}

`

)

response

.

end

}

break

case

"

POST

"

:

break

case

"

PUT

"

:

break

case

"

DELETE

"

:

break

default

:

// Send response for requests with no other response

response

.

statusCode

=

400

response

.

write

(

"

No Response

"

)

response

.

end

()

}

})

// get the server to start listening

server

.

listen

(

PORT

,

err

=>

{

// error checking

err

?

console

.

error

(

err

)

:

console

.

log

(

`listening on port

${

PORT

}

`

)

})

Enter fullscreen mode

Exit fullscreen mode

Now you can handle the request in infinite ways. The way I’m doing it is a switch statement based on the method followed by more switch statements based on url. This is the kind of thing that would already be handled by Koa/Fastify/Express’s routing logic. Another issue is we won’t have URL params since that isn’t build into node, that is done by some string/url parsing magic in our favorite frameworks which we could try to replicate but we won’t to keep this exercise manageable to understand.

Let’s Simplify This

Having a bunch of switches inside of switches may get a little harder to read, so let’s break out all the sub-switches into their own functions in another file.

  • touch get.js post.js put.js delete.js

get.js

module

.

exports

=

(

request

,

response

)

=>

{

switch

(

request

.

url

)

{

// response for unexpected get requests

default

:

response

.

statusCode

=

400

response

.

write

(

`CANNOT GET

${

request

.

url

}

`

)

response

.

end

()

}

}

Enter fullscreen mode

Exit fullscreen mode

post.js

module

.

exports

=

(

request

,

response

)

=>

{

switch

(

request

.

url

)

{

// response for unexpected get requests

default

:

response

.

statusCode

=

400

response

.

write

(

`CANNOT POST

${

request

.

url

}

`

)

response

.

end

()

}

}

Enter fullscreen mode

Exit fullscreen mode

put.js

module

.

exports

=

(

request

,

response

)

=>

{

switch

(

request

.

url

){

// response for unexpected get requests

default

:

response

.

statusCode

=

400

response

.

write

(

`CANNOT PUT

${

request

.

url

}

`

)

response

.

end

()

}

}

Enter fullscreen mode

Exit fullscreen mode

delete.js

module

.

exports

=

(

request

,

response

)

=>

{

switch

(

request

.

url

)

{

// response for unexpected get requests

default

:

response

.

statusCode

=

400

response

.

write

(

`CANNOT DELETE

${

request

.

url

}

`

)

response

.

end

()

}

}

Enter fullscreen mode

Exit fullscreen mode

Now let’s import these functions into server.js and clean it up, you can think of these four files as our “routers”.

server.js

// Import http library

const

http

=

require

(

"

http

"

)

// use env variable to define port with default

const

PORT

=

process

.

env

.

PORT

||

4000

// Import our routers

const

get

=

require

(

"

./get

"

)

const

post

=

require

(

"

./post

"

)

const

put

=

require

(

"

./put

"

)

// add an extra R since delete is a reserved word

const

deleteR

=

require

(

"

./delete

"

)

//create our server object, pass server function as callback argument

const

server

=

http

.

createServer

((

request

,

response

)

=>

{

// handle request based on method then URL

switch

(

request

.

method

)

{

case

"

GET

"

:

get

(

request

,

response

)

break

case

"

POST

"

:

post

(

request

,

response

)

break

case

"

PUT

"

:

put

(

request

,

response

)

break

case

"

DELETE

"

:

deleteR

(

request

,

response

)

break

default

:

// Send response for requests with no other response

response

.

statusCode

=

400

response

.

write

(

"

No Response

"

)

response

.

end

()

}

})

// get the server to start listening

server

.

listen

(

PORT

,

err

=>

{

// error checking

err

?

console

.

error

(

err

)

:

console

.

log

(

`listening on port

${

PORT

}

`

)

})

Enter fullscreen mode

Exit fullscreen mode

So now all our sub-switches are handled inside the function making our server.js cleaner and easier to read.

Our Dataset

To focus on just writing the API we aren’t bringing a database, so for a dataset we will just use an array of objects. This data will not persist meaning it will reset when you reset your server, this can always be fixed later by using a database, many to choose from.

  • create a file called data.js with the following

module

.

exports

=

[{

title

:

"

The first post

"

,

body

:

"

body of the first post

"

}]

Enter fullscreen mode

Exit fullscreen mode

import it into server.js, we will store this array of posts in the request object so all other routes will have access to it there since they are passed the request object.

// Import http library

const

http

=

require

(

"

http

"

)

// use env variable to define port with default

const

PORT

=

process

.

env

.

PORT

||

4000

// import data

const

posts

=

require

(

"

./data

"

)

// Import our routers

const

get

=

require

(

"

./get

"

)

const

post

=

require

(

"

./post

"

)

const

put

=

require

(

"

./put

"

)

// add an extra R since delete is a reserved word

const

deleteR

=

require

(

"

./delete

"

)

//create our server object, pass server function as callback argument

const

server

=

http

.

createServer

((

request

,

response

)

=>

{

// add the data to the request object so our routes can access it

request

.

posts

=

posts

// handle request based on method then URL

switch

(

request

.

method

)

{

case

"

GET

"

:

get

(

request

,

response

)

break

case

"

POST

"

:

post

(

request

,

response

)

break

case

"

PUT

"

:

put

(

request

,

response

)

break

case

"

DELETE

"

:

deleteR

(

request

,

response

)

break

default

:

// Send response for requests with no other response

response

.

statusCode

=

400

response

.

write

(

"

No Response

"

)

response

.

end

()

}

})

// get the server to start listening

server

.

listen

(

PORT

,

err

=>

{

// error checking

err

?

console

.

error

(

err

)

:

console

.

log

(

`listening on port

${

PORT

}

`

)

})

Enter fullscreen mode

Exit fullscreen mode

Ok… we’ve written a lot of code and haven’t written really any routes yet. See why we all love Koa/Express/Fastify (or even my obscure attempt, Merver).

Index Route

The index route is a get request to “/posts” that will return us the JSON of all the posts! We will create the route in get.js.

module

.

exports

=

(

request

,

response

)

=>

{

switch

(

request

.

url

)

{

case

"

/posts

"

:

response

.

statusCode

=

200

response

.

setHeader

(

"

Content-Type

"

,

"

application/json

"

)

response

.

write

(

JSON

.

stringify

(

request

.

posts

))

response

.

end

()

break

// response for unexpected get requests

default

:

response

.

statusCode

=

400

response

.

write

(

`CANNOT GET

${

request

.

url

}

`

)

response

.

end

()

}

}

Enter fullscreen mode

Exit fullscreen mode

The Show Route

Well, url queries and params aren’t handled out of the box in the nice tidy way we are use to in Koa/Fastify/Express. Params would require some heavy engineering to pull off so we’ll get by making queries available. We’ll store a URL object in the request object that we can use to get queries with.

server.js

// Import http library

const

http

=

require

(

"

http

"

);

// use env variable to define port with default

const

PORT

=

process

.

env

.

PORT

||

4000

;

// import the url standard library for parsing query string

require

(

"

url

"

)

// import data

const

posts

=

require

(

"

./data

"

);

// Import our routers

const

get

=

require

(

"

./get

"

);

const

post

=

require

(

"

./post

"

);

const

put

=

require

(

"

./put

"

);

// add an extra R since delete is a reserved word

const

deleteR

=

require

(

"

./delete

"

);

//create our server object, pass server function as callback argument

const

server

=

http

.

createServer

((

request

,

response

)

=>

{

// add the data to the request object so our routes can access it

request

.

posts

=

posts

// adding the query to the request object

request

.

query

=

new

URL

(

request

.

url

,

`http://

${

request

.

headers

.

host

}

`

)

// handle request based on method then URL

switch

(

request

.

method

)

{

case

"

GET

"

:

get

(

request

,

response

);

break

;

case

"

POST

"

:

post

(

request

,

response

);

break

;

case

"

PUT

"

:

put

(

request

,

response

);

break

;

case

"

DELETE

"

:

deleteR

(

request

,

response

);

break

;

default

:

// Send response for requests with no other response

response

.

statusCode

=

400

;

response

.

write

(

"

No Response

"

);

response

.

end

();

}

});

// get the server to start listening

server

.

listen

(

PORT

,

(

err

)

=>

{

// error checking

err

?

console

.

error

(

err

)

:

console

.

log

(

`listening on port

${

PORT

}

`

);

});

Enter fullscreen mode

Exit fullscreen mode

now we can add the show route which gets a particular item based on an id below (id will be based via url query “?id=0”).

get.js

module

.

exports

=

(

request

,

response

)

=>

{

// remove queries from the url, turn "/posts?id=0" into "/posts"

const

url

=

request

.

url

.

split

(

"

?

"

)[

0

]

switch

(

url

){

case

"

/posts

"

:

// if the id query is present return the show result

if

(

request

.

query

.

searchParams

.

get

(

"

id

"

)){

const

id

=

request

.

query

.

searchParams

.

get

(

"

id

"

)

response

.

statusCode

=

200

response

.

setHeader

(

"

Content-Type

"

,

"

application/json

"

)

response

.

write

(

JSON

.

stringify

(

request

.

posts

[

id

]))

response

.

end

()

}

else

{

// else return all posts (index)

response

.

statusCode

=

200

response

.

setHeader

(

"

Content-Type

"

,

"

application/json

"

)

response

.

write

(

JSON

.

stringify

(

request

.

posts

))

response

.

end

()

}

break

// response for unexpected get requests

default

:

response

.

statusCode

=

400

response

.

write

(

`CANNOT GET

${

request

.

url

}

`

)

response

.

end

()

break

}

}

Enter fullscreen mode

Exit fullscreen mode

The Create Route

Here is where we will really miss having a framework on our side as we parse the request body. We’re going to have to do what all those body parser middlewares do and work with a data stream.

  • create a getBody.js with the function that’ll act like a traditional middleware handling the request/response object then passing it to the next function in line.

module

.

exports

=

(

request

,

response

,

next

)

=>

{

let

data

=

[]

// assemble stream of data from request body

request

.

on

(

"

data

"

,

dataChunk

=>

{

data

.

push

(

dataChunk

)

})

request

.

on

(

"

end

"

,

()

=>

{

request

.

body

=

Buffer

.

concat

(

data

).

toString

()

if

(

request

.

headers

[

"

content-type

"

]

===

"

application/json

"

){

request

.

body

=

JSON

.

parse

(

request

.

body

)

}

// move on to next step in handling respone

next

(

request

,

response

)

})

}

Enter fullscreen mode

Exit fullscreen mode

Now let’s wrap our routing functions with this bodyParsing middleware in server.js

server.js

// Import http library

const

http

=

require

(

"

http

"

);

// use env variable to define port with default

const

PORT

=

process

.

env

.

PORT

||

4000

;

// import the url standard library for parsing query string

require

(

"

url

"

)

// import data

const

posts

=

require

(

"

./data

"

);

// Import our routers

const

get

=

require

(

"

./get

"

);

const

post

=

require

(

"

./post

"

);

const

put

=

require

(

"

./put

"

);

// add an extra R since delete is a reserved word

const

deleteR

=

require

(

"

./delete

"

);

// require function to parse body

const

getBody

=

require

(

"

./getBody

"

)

//create our server object, pass server function as callback argument

const

server

=

http

.

createServer

((

request

,

response

)

=>

{

// add the data to the request object so our routes can access it

request

.

posts

=

posts

// adding the query to the request object

request

.

query

=

new

URL

(

request

.

url

,

`http://

${

request

.

headers

.

host

}

`

)

// handle request based on method then URL

switch

(

request

.

method

)

{

case

"

GET

"

:

getBody

(

request

,

response

,

get

);

break

;

case

"

POST

"

:

getBody

(

request

,

response

,

post

);

break

;

case

"

PUT

"

:

getBody

(

request

,

response

,

put

);

break

;

case

"

DELETE

"

:

getBody

(

request

,

response

,

deleteR

);

break

;

default

:

// Send response for requests with no other response

response

.

statusCode

=

400

;

response

.

write

(

"

No Response

"

);

response

.

end

();

}

});

// get the server to start listening

server

.

listen

(

PORT

,

(

err

)

=>

{

// error checking

err

?

console

.

error

(

err

)

:

console

.

log

(

`listening on port

${

PORT

}

`

);

});

Enter fullscreen mode

Exit fullscreen mode

so now, regardless of method it will parse the body before passing the request and response to our routing functions. Now let’s make our create route which will allow us to send a json body via post request to “/posts”. You will need a tool like postman or insomnia to test this route.

post.js

module

.

exports

=

(

request

,

response

)

=>

{

switch

(

request

.

url

)

{

case

"

/posts

"

:

request

.

posts

.

push

(

request

.

body

);

response

.

statusCode

=

200

;

response

.

setHeader

(

"

Content-Type

"

,

"

application/json

"

);

response

.

write

(

JSON

.

stringify

(

request

.

posts

));

response

.

end

();

break

;

// response for unexpected get requests

default

:

response

.

statusCode

=

400

;

response

.

write

(

`CANNOT POST

${

request

.

url

}

`

);

response

.

end

();

}

};

Enter fullscreen mode

Exit fullscreen mode

Update Route

So we will use a url query again to specify id/index of the item to be updated. So in this case a put request to “/posts?id=x” will use the request body to update that object.

Since we already solved for url queries and the request body we just need to add the case to our put router function.

module

.

exports

=

(

request

,

response

)

=>

{

// remove queries from the url, turn "/posts?id=0" into "/posts"

const

url

=

request

.

url

.

split

(

"

?

"

)[

0

]

switch

(

url

){

case

"

/posts

"

:

const

id

=

request

.

query

.

searchParams

.

get

(

"

id

"

)

response

.

statusCode

=

200

response

.

setHeader

(

"

Content-Type

"

,

"

application/json

"

)

request

.

posts

[

id

]

=

request

.

body

response

.

write

(

JSON

.

stringify

(

request

.

posts

[

id

]))

response

.

end

()

break

// response for unexpected get requests

default

:

response

.

statusCode

=

400

response

.

write

(

`CANNOT PUT

${

request

.

url

}

`

)

response

.

end

()

break

}

}

Enter fullscreen mode

Exit fullscreen mode

Destroy Route

By making a delete request to “/posts?id=x” you should be able to delete any item from the array of posts.

delete.js

module

.

exports

=

(

request

,

response

)

=>

{

// remove queries from the url, turn "/posts?id=0" into "/posts"

const

url

=

request

.

url

.

split

(

"

?

"

)[

0

];

switch

(

url

)

{

case

"

/posts

"

:

const

id

=

request

.

query

.

searchParams

.

get

(

"

id

"

);

response

.

statusCode

=

200

;

response

.

setHeader

(

"

Content-Type

"

,

"

application/json

"

);

request

.

posts

.

splice

(

id

,

1

);

response

.

write

(

JSON

.

stringify

(

request

.

posts

));

response

.

end

();

break

;

// response for unexpected get requests

default

:

response

.

statusCode

=

400

;

response

.

write

(

`CANNOT DELETE

${

request

.

url

}

`

);

response

.

end

();

break

;

}

};

Enter fullscreen mode

Exit fullscreen mode

Conclusion

Well, we created a very crude full crud json api using raw node and no frameworks like Express, KOA, or Fastify or any of the robust frameworks built on top of them. We would still need to handle a lot more to get to the same level of basic functionaltiy.

  • creating routing params
  • setting up cors headers
  • being able to parse urlEncoded or XML bodies
  • adding https support with the “https” library

So while I doubt you’ll be making a raw api like this again anytime soon. I hope having done this has given you a deeper appreciation for the abstractions and patterns you’ll find in express, koa and fastify.