Skip to content

RESTful API

HTTP

Client, like a web browser and server communicates using a HyperText Transfer Protocol (HTTP). The client sends a HTTP request to server side, which includes URL identifying the target server and also the method that defines the action required. HTTP Methods are also referred to as HTTP verbs.

Some commonly used methods and their associated actions are (full list of HTTP request methods):

Method Description
GET Get resource from server
POST Create a new resource
PUT Update an existing resource, or create if not exists
DELETE Delete an existing resource
PATCH Update a specified resource

Server responds to the client request by providing the requested data or an error if the requested resource is not available.

sequenceDiagram Client->>+Server: HTTP request Note right of Client: GET /api/resource/:id Server->>+Client: HTTP response Note right of Client: JSON {...}

We are mostly working with a JSON data in this course. Client, like a browser, Postman or Visual Studio Code - Rest Client is sending a request and server is responding with a JSON data.

REST

REST (REpresentational State Transfer) is an architectural style to provide communication between different computer systems. It is mostly used between the client and the server communication in web application. The main idea is that the client and the server is working independently without knowing about the other one. Communication that uses the REST are said to be stateless, which means that the server does not need to know anything about what state the client has and vice versa.

REST was defined in 2000 by Roy Fielding's dissertation. It’s not a standard but a set of recommendations and constraints for RESTful web services, which includes following:

  • Uniform interface, Defines the interface between client and server, fundametal to RESTful design.
  • Client-Server, Client makes an HTTP request to a URL hosted by Server, which returns a response.
  • Stateless, Client request should contain all the information necessary to respond to a request.
  • Cacheable, Response should be defined as cacheable or not.
  • Layered system, Requesting client don't need to know whether it’s communicating with the actual server, a proxy, or any other intermediary.
  • Code on demand, Server can temporarily extend client, Transfer logic to client, client executes logic.

One good link: What is RESTful API?

In this architecture, a REST server provides connectivity to resources, which helps client to access server application data. These resources are recognized by the URIs. In a web applications, these resources are used with HTTP-protocol requests.

In a server side, a server will receive requests, authenticate, processes them and returns a response to the caller for example with text, HTML, JSON or XML format.

Routes

You will need to create routes to your Node.js application. Routing refers to determining how a server side application responds to a client request to a particular endpoint, which is a URI (or path) and a specific HTTP request method (GET, POST, and so on). Each route can have one or more handler functions, which are executed when the route is matched. These handler functions can return text messages, HTML page or somekind of data like JSON, etc... to caller client. In a larger application you would have several routes to handle several URI's.

In other words, the application "listens" for requests that match the specified route(s) and method(s), and when it detects a match, it calls the specified callback function.

Route definition takes the following structure:

1
2
3
4
5
6
7
const express = require('express') 
const app = express()

// METHOD is the HTTP method of the request, such as GET, PUT, POST, etc...
// PATH is a path on the server.
// HANDLER is the function executed when the route is matched.
app.METHOD(PATH, HANDLER)

Example GET request to /hello path to respond with 'Hello Express!' text.

1
2
3
app.get('/hello', (request, response) => {
  response.send('Hello Express!')
})

Routing methods

For a full list, see app.METHOD. You can also use app.all() to handle all HTTP methods.

Usually application describes CRUD requests:

  • POST request sends data (C, create)
  • GET request get’s data (R, read)
  • PUT request updates data (U, update)
  • DELETE request deletes data (D, delete)

Path

Route paths, in combination with a request method, define the endpoints at which requests can be made. Route paths can be strings, string patterns, or regular expressions.

Here are a few examples:

1
2
3
app.get('/', (request, response) => {
  response.send('about')
})
1
2
3
app.get('/login', (request, response) => {
  response.send('My login route')
})
1
2
3
4
// route path will match /abe and /abcde
app.get(/ab(cd)?e/, (request, response) => {
  response.send('regex')
})
1
2
3
app.put('/users/:id', (request, response) => {
  response.send('Got a PUT request at /users/:id')
})
1
2
3
app.delete('/users/:id', (request, response) => {
  response.send('Got a DELETE request at /users/:id')
})

Parameters

Route parameters are named URL segments that are used to capture the values specified at their position in the URL. The captured values are populated in the req.params object, with the name of the route parameter specified in the path as their respective keys.

For example get request with users and todos:

1
Request URL: http://localhost:3000/users/10/todos/231

To define routes with route parameters, simply specify the route parameters in the path of the route as shown below.

1
2
3
4
app.get('/users/:userId/todos/:todoId', (request, response) => {
  // code here... now only send request params back to the caller
  response.send(request.params)
})

Route path is now

1
/users/:userId/todos/:todoId

And params are inside request object.

1
request.params: { "userId": "10", "todoId": "231" }

Note

The name of route parameters must be made up of “word characters” ([A-Za-z0-9_]).

Handler callback

These routing methods specify a callback function (sometimes called "handler functions") called when the application receives a request to the specified route (endpoint) and HTTP method. In a below callback function only send response back to the caller.

1
2
3
app.get('/about', function (request, response) {
  response.send('about')
})

Or you can use array functions:

1
2
3
app.get('/about', (request, response) => {
  response.send('about')
})

The routing methods can have more than one callback function as arguments. With multiple callback functions, it is important to provide next as an argument to the callback function and then call next() within the body of the function to hand off control to the next callback.

1
2
3
4
5
6
7
app.get('/', (request, response, next) => {
  console.log('Executing the 1st callback function')
  next()
}, (request, response) => {
  console.log('Executing the 2nd callback function')
  response.send('Hello Express!')
})

Create a request to your localhost and you should see a 'Hello Express!' text in your browser and folloing text lines in your terminal.

1
2
3
Example app listening on port 3000
Executing the 1st callback function
Executing the 2nd callback function

Note

Remember send response to the client with one of the following Response methods or the client request will be left hanging.

Example - GET

Create a single GET route to your application:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const express = require('express') 
const app = express()
const port = 3000

app.get('/', (request, response) => {
  response.send('Hello from server side!')
})

app.listen(port, () => {
  console.log('Example app listening on port 3000')
})

In above example, first parameter in the get function is the route and second one is the event handler which handles all the HTTP GET calls to this route.

Now the response object is used with send method to send string content back to client application / browser. Express will automatically add content-type header value as a text/html and status code 200.

Image 01

Test now http://localhost:3000 address in the browser and you should see Hello Express! text in your browser. If you test some other "routes" like: http://localhost:3000/test, it won't work because that route is not served. You should get Cannot GET /test text.

Lets add one more route to our app. Modify your application support /test route too.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const express = require('express') 
const app = express()
const port = 3000

app.get('/', (request, response) => {
  response.send('Hello Express!')
})

app.get('/test', (request, response) => {
  response.send('Test route!')
})

app.listen(port, () => {
  console.log('Example app listening on port 3000')
})

Now both of the routes should work http://localhost:3000 and http://localhost:3000/test.

Remember:

  • The request object is particular HTTP request from the client.
  • The response object contains all the information how and what server is responding to the client.

Create one more route, which returns some JSON to the caller browser. Now you can use json method with response object. This json method sends JSON string back to caller browser and Express set header Content-type value as a application/json automatically.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const express = require('express') 
const app = express()
const port = 3000

let data = {'name':'Kirsi Kernel'}

app.get('/person', (request, response) => {
  response.json(data)
})

//...

Try now http://localhost:3000/json and check returned Content-Type from your browser's Inspector Network tab. It should be Content-Type: application/json; charset=utf-8.

Example - POST, PUT, DELETE

Add the following GET, POST, PUT and DELETE routes to your application where URI (path) doesn't change, but different string value will be send back to the caller. Now you aren't sending any real data to the server side, just testing different methods from Postman.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const express = require('express') 
const app = express()
const port = 3000

app.get('/', (req, res) => {
  res.send('GET HTTP received!')
})

app.post('/', (req, res) => {
  res.send('POST HTTP received!')
});

app.put('/', (req, res) => {
  res.send('PUT HTTP received!')
});

app.delete('/', (req, res) => {
  res.send('DELETE HTTP received!')
})

app.listen(port, () => {
  console.log('Example app listening on port 3000')
})

Note

You will create more routes in exercises.

Use Postman to test different RESTful HTTP request to your server.

!Image 02

Versioning

Express doesn’t offer built-in support for versioning routes. APIs only need to be up-versioned when a breaking changes are made:

  • a change in the format of the response data for one or more calls
  • a change in the request or response type (variable type)
  • removing any part of the API

Note

There are mixed opinions around whether an API version should be included in the URL or in a header. Here are a few examples how you can do it.

Commonly used API versioning strategies are to use prefixing like /v1 and /v2 in the front of your resource name. Send requeset with correct URL to v1:

1
GET http://localhost:3000/v1/users

or v2:

1
GET http://localhost:3000/v2/users

And create end points with Express:

1
2
3
4
5
6
7
app.get('/v1/users', (request, response) => {
  response.send('Users v1')
})

app.get('/v2/users', (request, response) => {
  response.send('Users v2')
})

Another way is to use request parameters with query params. Send request with params:

1
GET http://localhost:3000/users?version=1.0

And check request params in Express:

1
2
3
4
5
6
app.get('/users', (request, response) => {
  const { version } = request.query
  if (version === "1.0") response.send('Users v1')
  else if (version === "2.0") response.send('Users v2')
  else response.status(400).json({ error: 'Bad API version call' })
})

Or add a custom HTTP headers to your request:

1
2
GET http://localhost:3000/users
Accept-version: 1.0 

And check headers in Express:

1
2
3
4
5
6
7
8
app.get('/users', (request, response) => {
  if (request.headers['accept-version'] === '1.0') 
    response.send('Users v1')
  else if (request.headers['accept-version'] === '2.0') 
    response.send('Users v2')
  else 
    response.status(400).json({ error: 'Bad API version call' })
})

Use 3rd party npm's to handle versioning:

Example: use express-version-route and express-version-request npm:s.

Create a request:

1
2
GET http://localhost:3000/test
X-Api-Version: 1.0

And use Express and npms to detect different api version calls:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// npm: express-version-request
const versionRequest = require('express-version-request')
// parse the version out of the X-Api-Version HTTP header
app.use(versionRequest.setVersionByHeader())

// npm: express-version-route
const versionRouter = require('express-version-route')

// create routes map with versions 1.0, 2.0 and default
const routesMap = new Map()

routesMap.set('1.0', (req, res) => {
  return res.status(200).json({'message': 'Version 1.0'})
})

routesMap.set('2.0', (req, res) => {
  return res.status(200).json({'message': 'Version 2.0'})
})

// add /test endpoint to version routes
app.get('/test', versionRouter.route(routesMap))

Use HTTP status codes correctly

If something goes wrong while serving a request in a server side, set the correct status code for the response:

  • 2xx, everything okay
  • 3xx, resource was moved
  • 4xx, request can't be fulfilled, client error (like requesting a resource that does not exist)
  • 5xx, error in API side

List of HTTP response status codes

Using environmental variables

Now we are used only hardcoded values in your code - like application port. In a real-life applications we have configuration that varies from environment to environment - from local development all the way to the production environment.

You can use dotenv package, which is a zero-dependency module that loads environment variables from a .env file into process.env. You can use process.env property to return your value.

You can install dotenv as a dependency:

1
npm install dotenv

Now dotenv will be included your package.json dependencies:

1
2
3
4
5
6
7
{
  //...
  "dependencies": {
    "express": "^4.17.1",
    "dotenv": "^8.2.0"
  }
}

Create .env file to your project folder and add a port number used.

1
PORT=3000

Then in your index.js file you'll require it.

1
2
require('dotenv').config()
const express = require("express")

Modify your code to use defined port.

1
2
3
app.listen(process.env.PORT, () => {
  console.log('Example app listening on port 3000')
})

Environment variables are used things like credentials, which is not wanted to be exposed to the outside world.

Note

Remember add .env to .gitignore file.

Read more

Goals of this topic

Understand

  • What Express is.
  • Know how to create a simple Express application.
  • Know how to use routing in Express.
  • Know what environmental variables are and how to use them.