Swiftorial Logo
Home
Swift Lessons
Matchups
CodeSnaps
Tutorials
Career
Resources

Express.js API Development

API development is a fundamental aspect of modern web applications. Express.js provides a robust framework for building RESTful APIs. This guide covers key concepts, examples, and best practices for developing APIs with Express.js.

Key Concepts of API Development

  • REST (Representational State Transfer): An architectural style for designing networked applications using standard HTTP methods.
  • CRUD Operations: Create, Read, Update, Delete operations that form the basis of most APIs.
  • Routes: Endpoints that define how the API responds to different HTTP requests.
  • Middleware: Functions that process requests and responses in the API lifecycle.

Setting Up an Express.js API

Create a basic Express.js API server that handles CRUD operations:

Example: Basic API Server

// api-server.js
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
const port = 3000;

let items = [];

// Middleware setup
app.use(bodyParser.json());

// Routes
app.get('/api/items', (req, res) => {
    res.json(items);
});

app.post('/api/items', (req, res) => {
    const newItem = { id: items.length + 1, ...req.body };
    items.push(newItem);
    res.status(201).json(newItem);
});

app.get('/api/items/:id', (req, res) => {
    const item = items.find(i => i.id === parseInt(req.params.id));
    if (!item) return res.status(404).send('Item not found');
    res.json(item);
});

app.put('/api/items/:id', (req, res) => {
    const item = items.find(i => i.id === parseInt(req.params.id));
    if (!item) return res.status(404).send('Item not found');
    Object.assign(item, req.body);
    res.json(item);
});

app.delete('/api/items/:id', (req, res) => {
    const itemIndex = items.findIndex(i => i.id === parseInt(req.params.id));
    if (itemIndex === -1) return res.status(404).send('Item not found');
    const deletedItem = items.splice(itemIndex, 1);
    res.json(deletedItem);
});

app.listen(port, () => {
    console.log(`API server running at http://localhost:${port}/`);
});

Using a Database for Persistent Storage

Integrate a database for persistent storage. Here, we'll use MongoDB with Mongoose:

Installation

npm install mongoose --save

Example: Integrating MongoDB

// api-server-mongo.js
const express = require('express');
const bodyParser = require('body-parser');
const mongoose = require('mongoose');
const app = express();
const port = 3000;

// Connect to MongoDB
mongoose.connect('mongodb://localhost:27017/mydatabase', { useNewUrlParser: true, useUnifiedTopology: true });

const db = mongoose.connection;
db.on('error', console.error.bind(console, 'connection error:'));
db.once('open', () => {
    console.log('Connected to MongoDB');
});

// Define a schema and model
const itemSchema = new mongoose.Schema({
    name: String,
    description: String,
});

const Item = mongoose.model('Item', itemSchema);

// Middleware setup
app.use(bodyParser.json());

// Routes
app.get('/api/items', async (req, res) => {
    const items = await Item.find();
    res.json(items);
});

app.post('/api/items', async (req, res) => {
    const newItem = new Item(req.body);
    await newItem.save();
    res.status(201).json(newItem);
});

app.get('/api/items/:id', async (req, res) => {
    const item = await Item.findById(req.params.id);
    if (!item) return res.status(404).send('Item not found');
    res.json(item);
});

app.put('/api/items/:id', async (req, res) => {
    const item = await Item.findByIdAndUpdate(req.params.id, req.body, { new: true });
    if (!item) return res.status(404).send('Item not found');
    res.json(item);
});

app.delete('/api/items/:id', async (req, res) => {
    const item = await Item.findByIdAndDelete(req.params.id);
    if (!item) return res.status(404).send('Item not found');
    res.json(item);
});

app.listen(port, () => {
    console.log(`API server running at http://localhost:${port}/`);
});

Adding Authentication to Your API

Secure your API endpoints using JSON Web Tokens (JWT):

Installation

npm install jsonwebtoken bcryptjs --save

Example: JWT Authentication

// api-server-jwt.js
const express = require('express');
const bodyParser = require('body-parser');
const mongoose = require('mongoose');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const app = express();
const port = 3000;

const secretKey = 'secretkey';

// Connect to MongoDB
mongoose.connect('mongodb://localhost:27017/mydatabase', { useNewUrlParser: true, useUnifiedTopology: true });

const db = mongoose.connection;
db.on('error', console.error.bind(console, 'connection error:'));
db.once('open', () => {
    console.log('Connected to MongoDB');
});

// Define schemas and models
const userSchema = new mongoose.Schema({
    username: String,
    password: String,
});

const itemSchema = new mongoose.Schema({
    name: String,
    description: String,
});

const User = mongoose.model('User', userSchema);
const Item = mongoose.model('Item', itemSchema);

// Middleware setup
app.use(bodyParser.json());

const authenticateJWT = (req, res, next) => {
    const token = req.headers.authorization;
    if (token) {
        jwt.verify(token, secretKey, (err, user) => {
            if (err) {
                return res.sendStatus(403);
            }
            req.user = user;
            next();
        });
    } else {
        res.sendStatus(401);
    }
};

// Routes
app.post('/register', async (req, res) => {
    const { username, password } = req.body;
    const hashedPassword = bcrypt.hashSync(password, 10);
    const user = new User({ username, password: hashedPassword });
    await user.save();
    res.send('User registered');
});

app.post('/login', async (req, res) => {
    const { username, password } = req.body;
    const user = await User.findOne({ username });
    if (!user || !bcrypt.compareSync(password, user.password)) {
        return res.status(400).send('Invalid username or password');
    }
    const token = jwt.sign({ username: user.username }, secretKey, { expiresIn: '1h' });
    res.json({ token });
});

app.get('/api/items', authenticateJWT, async (req, res) => {
    const items = await Item.find();
    res.json(items);
});

app.post('/api/items', authenticateJWT, async (req, res) => {
    const newItem = new Item(req.body);
    await newItem.save();
    res.status(201).json(newItem);
});

app.get('/api/items/:id', authenticateJWT, async (req, res) => {
    const item = await Item.findById(req.params.id);
    if (!item) return res.status(404).send('Item not found');
    res.json(item);
});

app.put('/api/items/:id', authenticateJWT, async (req, res) => {
    const item = await Item.findByIdAndUpdate(req.params.id, req.body, { new: true });
    if (!item) return res.status(404).send('Item not found');
    res.json(item);
});

app.delete('/api/items/:id', authenticateJWT, async (req, res) => {
    const item = await Item.findByIdAndDelete(req.params.id);
    if (!item) return res.status(404).send('Item not found');
    res.json(item);
});

app.listen(port, () => {
    console.log(`API server running at http://localhost:${port}/`);
});

Documenting Your API

Use Swagger to document your API:

Installation

npm install swagger-ui-express swagger-jsdoc --save

Example: Swagger Documentation

// api-server-swagger.js
const express = require('express');
const bodyParser = require('body-parser');
const mongoose = require('mongoose');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const swaggerUi = require('swagger-ui-express');
const swaggerJsdoc = require('swagger-jsdoc');
const app = express();
const port = 3000;

const secretKey = 'secretkey';

// Swagger setup
const options = {
    swaggerDefinition: {
        openapi: '3.0.0',
        info: {
            title: 'Express.js API',
            version: '1.0.0',
            description: 'A simple Express.js API',
        },
        servers: [
            {
                url: 'http://localhost:3000',
            },
        ],
    },
    apis: ['./api-server-swagger.js'],
};

const specs = swaggerJsdoc(options);
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs));

// Connect to MongoDB
mongoose.connect('mongodb://localhost:27017/mydatabase', { useNewUrlParser: true, useUnifiedTopology: true });

const db = mongoose.connection;
db.on('error', console.error.bind(console, 'connection error:'));
db.once('open', () => {
    console.log('Connected to MongoDB');
});

// Define schemas and models
const userSchema = new mongoose.Schema({
    username: String,
    password: String,
});

const itemSchema = new mongoose.Schema({
    name: String,
    description: String,
});

const User = mongoose.model('User', userSchema);
const Item = mongoose.model('Item', itemSchema);

// Middleware setup
app.use(bodyParser.json());

const authenticateJWT = (req, res, next) => {
    const token = req.headers.authorization;
    if (token) {
        jwt.verify(token, secretKey, (err, user) => {
            if (err) {
                return res.sendStatus(403);
            }
            req.user = user;
            next();
        });
    } else {
        res.sendStatus(401);
    }
};

/**
 * @swagger
 * components:
 *   schemas:
 *     Item:
 *       type: object
 *       required:
 *         - name
 *         - description
 *       properties:
 *         id:
 *           type: string
 *           description: The auto-generated id of the item
 *         name:
 *           type: string
 *           description: The name of the item
 *         description:
 *           type: string
 *           description: The description of the item
 *       example:
 *         id: d5fE_asz
 *         name: Item name
 *         description: Item description
 */

/**
 * @swagger
 * tags:
 *   name: Items
 *   description: The items managing API
 */

/**
 * @swagger
 * /api/items:
 *   get:
 *     summary: Returns the list of all the items
 *     tags: [Items]
 *     responses:
 *       200:
 *         description: The list of the items
 *         content:
 *           application/json:
 *             schema:
 *               type: array
 *               items:
 *                 $ref: '#/components/schemas/Item'
 */
app.get('/api/items', authenticateJWT, async (req, res) => {
    const items = await Item.find();
    res.json(items);
});

/**
 * @swagger
 * /api/items:
 *   post:
 *     summary: Create a new item
 *     tags: [Items]
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             $ref: '#/components/schemas/Item'
 *     responses:
 *       201:
 *         description: The item was successfully created
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/Item'
 *       500:
 *         description: Some server error
 */
app.post('/api/items', authenticateJWT, async (req, res) => {
    const newItem = new Item(req.body);
    await newItem.save();
    res.status(201).json(newItem);
});

/**
 * @swagger
 * /api/items/{id}:
 *   get:
 *     summary: Get the item by id
 *     tags: [Items]
 *     parameters:
 *       - in: path
 *         name: id
 *         schema:
 *           type: string
 *         required: true
 *         description: The item id
 *     responses:
 *       200:
 *         description: The item description by id
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/Item'
 *       404:
 *         description: The item was not found
 */
app.get('/api/items/:id', authenticateJWT, async (req, res) => {
    const item = await Item.findById(req.params.id);
    if (!item) return res.status(404).send('Item not found');
    res.json(item);
});

/**
 * @swagger
 * /api/items/{id}:
 *  put:
 *    summary: Update the item by the id
 *    tags: [Items]
 *    parameters:
 *      - in: path
 *        name: id
 *        schema:
 *          type: string
 *        required: true
 *        description: The item id
 *    requestBody:
 *      required: true
 *      content:
 *        application/json:
 *          schema:
 *            $ref: '#/components/schemas/Item'
 *    responses:
 *      200:
 *        description: The item was updated
 *        content:
 *          application/json:
 *            schema:
 *              $ref: '#/components/schemas/Item'
 *      404:
 *        description: The item was not found
 *      500:
 *        description: Some error happened
 */
app.put('/api/items/:id', authenticateJWT, async (req, res) => {
    const item = await Item.findByIdAndUpdate(req.params.id, req.body, { new: true });
    if (!item) return res.status(404).send('Item not found');
    res.json(item);
});

/**
 * @swagger
 * /api/items/{id}:
 *   delete:
 *     summary: Remove the item by id
 *     tags: [Items]
 *     parameters:
 *       - in: path
 *         name: id
 *         schema:
 *           type: string
 *         required: true
 *         description: The item id
 *     responses:
 *       200:
 *         description: The item was deleted
 *       404:
 *         description: The item was not found
 */
app.delete('/api/items/:id', authenticateJWT, async (req, res) => {
    const item = await Item.findByIdAndDelete(req.params.id);
    if (!item) return res.status(404).send('Item not found');
    res.json(item);
});

app.listen(port, () => {
    console.log(`API server running at http://localhost:${port}/`);
});

Best Practices for API Development

  • Use RESTful Principles: Follow RESTful design principles to ensure your API is intuitive and consistent.
  • Version Your API: Implement versioning to manage changes and maintain backward compatibility.
  • Secure Your API: Implement authentication and authorization to protect your API endpoints.
  • Validate Inputs: Validate and sanitize user inputs to prevent injection attacks.
  • Document Your API: Provide clear and comprehensive documentation for your API.
  • Handle Errors Gracefully: Implement robust error handling and provide meaningful error messages.

Testing Your API

Test your API endpoints using frameworks like Mocha, Chai, and Supertest:

Example: Testing API Endpoints

// Install Mocha, Chai, and Supertest
// npm install --save-dev mocha chai supertest

// test/api.test.js
const chai = require('chai');
const expect = chai.expect;
const request = require('supertest');
const express = require('express');
const bodyParser = require('body-parser');
const mongoose = require('mongoose');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');

const app = express();
const secretKey = 'secretkey';

// Connect to MongoDB for testing
mongoose.connect('mongodb://localhost:27017/testdatabase', { useNewUrlParser: true, useUnifiedTopology: true });

const userSchema = new mongoose.Schema({
    username: String,
    password: String,
});

const itemSchema = new mongoose.Schema({
    name: String,
    description: String,
});

const User = mongoose.model('User', userSchema);
const Item = mongoose.model('Item', itemSchema);

app.use(bodyParser.json());

const authenticateJWT = (req, res, next) => {
    const token = req.headers.authorization;
    if (token) {
        jwt.verify(token, secretKey, (err, user) => {
            if (err) {
                return res.sendStatus(403);
            }
            req.user = user;
            next();
        });
    } else {
        res.sendStatus(401);
    }
};

// Routes for testing
app.post('/register', async (req, res) => {
    const { username, password } = req.body;
    const hashedPassword = bcrypt.hashSync(password, 10);
    const user = new User({ username, password: hashedPassword });
    await user.save();
    res.send('User registered');
});

app.post('/login', async (req, res) => {
    const { username, password } = req.body;
    const user = await User.findOne({ username });
    if (!user || !bcrypt.compareSync(password, user.password)) {
        return res.status(400).send('Invalid username or password');
    }
    const token = jwt.sign({ username: user.username }, secretKey, { expiresIn: '1h' });
    res.json({ token });
});

app.get('/api/items', authenticateJWT, async (req, res) => {
    const items = await Item.find();
    res.json(items);
});

app.post('/api/items', authenticateJWT, async (req, res) => {
    const newItem = new Item(req.body);
    await newItem.save();
    res.status(201).json(newItem);
});

app.get('/api/items/:id', authenticateJWT, async (req, res) => {
    const item = await Item.findById(req.params.id);
    if (!item) return res.status(404).send('Item not found');
    res.json(item);
});

app.put('/api/items/:id', authenticateJWT, async (req, res) => {
    const item = await Item.findByIdAndUpdate(req.params.id, req.body, { new: true });
    if (!item) return res.status(404).send('Item not found');
    res.json(item);
});

app.delete('/api/items/:id', authenticateJWT, async (req, res) => {
    const item = await Item.findByIdAndDelete(req.params.id);
    if (!item) return res.status(404).send('Item not found');
    res.json(item);
});

describe('POST /register', () => {
    it('should register a new user', (done) => {
        request(app)
            .post('/register')
            .send({ username: 'John', password: 'password' })
            .expect(200, 'User registered', done);
    });
});

describe('POST /login', () => {
    it('should log in a user and return a token', (done) => {
        request(app)
            .post('/login')
            .send({ username: 'John', password: 'password' })
            .expect('Content-Type', /json/)
            .expect(200)
            .end((err, res) => {
                if (err) return done(err);
                expect(res.body).to.have.property('token');
                done();
            });
    });

    it('should return 400 for invalid credentials', (done) => {
        request(app)
            .post('/login')
            .send({ username: 'invalid', password: 'invalid' })
            .expect(400, 'Invalid username or password', done);
    });
});

// Define test script in package.json
// "scripts": {
//   "test": "mocha"
// }

// Run tests with NPM
// npm run test

Key Points

  • REST (Representational State Transfer): An architectural style for designing networked applications using standard HTTP methods.
  • CRUD Operations: Create, Read, Update, Delete operations that form the basis of most APIs.
  • Routes: Endpoints that define how the API responds to different HTTP requests.
  • Follow best practices for API development, such as using RESTful principles, versioning your API, securing your API, validating inputs, documenting your API, and handling errors gracefully.

Conclusion

API development is a fundamental aspect of modern web applications. By understanding and implementing the key concepts, examples, and best practices covered in this guide, you can effectively develop robust and secure APIs with Express.js. Happy coding!