← Назад

How to Build Your First REST API from Scratch: A Beginner-Friendly, End-to-End Tutorial

What a REST API Really Is

A REST API is nothing more than a web server that speaks plain HTTP and returns JSON. Instead of sending back styled pages like a traditional website, it sends raw data your apps can consume in any language or platform. Think of it as a vending machine: you press the right button with the right coins (an HTTP request) and out pops your snack (JSON data).

This tutorial will walk you through the entire journey—from a blank folder to a fully working API running on the internet.

The Project You Will Build

We will create a small notes API that lets clients:

  • create notes,
  • list every note,
  • get a single note,
  • update a note,
  • delete a note.

Simple, but complete enough to show all core REST patterns you will reuse in real projects.

Tools You Need

  • Node.js 18 or newer (includes npm)
  • Git for version control
  • cURL or Postman for manual testing
  • A free Render or Railway account for deployment

All tools are cross-platform; install them once and you are set.

Step 1: Create a Git Repo and Folder Structure

Open your terminal and run:

mkdir notes-api && cd notes-api
git init
npm init -y

Add folders so you do not dump everything into one place:

mkdir src routes models middleware tests

Inside src, place all application code. We will keep the root tidy for scripts and configuration.

Step 2: Install Core Dependencies

The ecosystem offers many libraries, but you only need two for a minimal, robust service:

npm install express
dotenv
npm install --save-dev nodemon
  • Express is a tiny web framework that handles routing for us.
  • dotenv reads variables from .env files (API keys, ports) instead of hard-coding.
  • nodemon restarts your server on file changes during development.

Step 3: Create a .env File

# .env
PORT=4000
NODE_ENV=development

Never push secrets to Git. Add .env to your .gitignore.

Step 4: Spin Up a Basic Express Server

Create src/index.js:

require('dotenv').config()
const express = require('express')
const app = express()

const PORT = process.env.PORT || 4000

app.use(express.json())   // parse incoming JSON

app.get('/', (req, res) => {
  res.json({ message: 'Notes API up and running.' })
})

app.listen(PORT, () =>
  console.log(`Server listening at http://localhost:${PORT}`)
)

Test it quickly:

npm pkg set scripts.dev="nodemon src/index.js"
npm run dev

Open http://localhost:4000 in your browser and you should see the welcome message.

Step 5: Design Your Data Model

Even small side projects benefit from clear separation. Store notes in memory for now so you focus on routes before wrestling with a database.

// models/Note.js
let idCounter = 1
const notes = []

exports.createNote = ({ title, body }) => {
  const note = { id: idCounter++, title, body }
  notes.push(note)
  return note
}

exports.getAllNotes = () => notes
exports.getNote = id => notes.find(n => n.id === id)
exports.updateNote = (id, patch) => {
  const note = exports.getNote(id)
  if (!note) return null
  Object.assign(note, patch)
  return note
}
exports.deleteNote = id => {
  const idx = notes.findIndex(n => n.id === id)
  if (idx === -1) return false
  notes.splice(idx, 1)
  return true
}

This file is plain JavaScript—no ORM, no magic.

Step 6: Write REST Routes

REST is discussed endlessly, but in practice you only follow three rules:

  1. Use HTTP verbs correctly: GET read, POST create, PUT/PATCH update, DELETE remove.
  2. Use meaningful URLs such as /notes and /notes/:id.
  3. Return proper status codes (200, 201, 204, 400, 404, 500).

Create routes/notes.js:

const router = require('express').Router()
const Note = require('../models/Note')

router.post('/', (req, res) => {
  const { title, body } = req.body
  if (!title || !body)
    return res.status(400).json({ error: 'title and body required' })
  const note = Note.createNote({ title, body })
  res.status(201).json(note)
})

router.get('/', (_, res) => {
  const notes = Note.getAllNotes()
  res.json(notes)
})

router.get('/:id', (req, res) => {
  const note = Note.getNote(Number(req.params.id))
  if (!note) return res.sendStatus(404)
  res.json(note)
})

router.patch('/:id', (req, res) => {
  const note = Note.updateNote(Number(req.params.id), req.body)
  if (!note) return res.sendStatus(404)
  res.json(note)
})

router.delete('/:id', (req, res) => {
  const ok = Note.deleteNote(Number(req.params.id))
  if (!ok) return res.sendStatus(404)
  res.sendStatus(204)
})

module.exports = router

Wire the router into src/index.js:

const notesRouter = require('./routes/notes')
app.use('/notes', notesRouter)

Restart the server with npm run dev.

Step 7: Test Your API Manually

Use cURL (available on every operating system). Each command should succeed:

# create
curl -X POST -H "Content-Type: application/json" \
     -d '{"title":"Buy milk","body":"2% organic"}' \
     http://localhost:4000/notes

# list all
curl http://localhost:4000/notes

# get single (replace ID)
curl http://localhost:4000/notes/1

# update the title
curl -X PATCH -H "Content-Type: application/json" \
     -d '{"title":"Buy almond milk"}' \
     http://localhost:4000/notes/1

# removedelete
curl -X DELETE http://localhost:4000/notes/1

Seeing the JSON responses confirms that every route behaves as expected.

Step 8: Add Automated Tests

Manual checks are quick at first, but tests guarantee changes do not break your contract.

npm install --save-dev jest supertest

Create tests/notes.test.js:

const request = require('supertest')
const app = require('../src/index')

describe('Notes API', () => {
  it('should create a note', () =>
    request(app)
      .post('/notes')
      .send({ title: 'T', body: 'B' })
      .expect(201)
      .then(res => {
        expect(res.body.id).toBeGreaterThan(0)
      }))

  it('should list notes', () =>
    request(app).get('/notes').expect(200))
})

Note: supertest directly imports src/index.js but does NOT start the HTTP listener; we modify our index slightly to export the app object after the last line:

module.exports = app

Add a test script:

npm pkg set scripts.test="jest --watchAll"
npm test

The tests should turn green immediately.

Step 9: Swap In-Memory Storage for SQLite

No real API keeps data in arrays. With tests in place you can refactor safely.

npm install sqlite3

Create src/database.js:

const sqlite3 = require('sqlite3')
const path = require('path')
module.exports = new sqlite3.Database(path.join(__dirname, 'data.sqlite'))

Update models/Note.js to use SQL instead:

const db = require('../database')
db.serialize(() =>
  db.run(
    `CREATE TABLE IF NOT EXISTS notes (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        title TEXT NOT NULL,
        body TEXT NOT NULL
     )`
  )
)

exports.createNote = ({ title, body }) =>
  new Promise(resolve => {
    const stmt = db.prepare('INSERT INTO notes(title, body) VALUES(?, ?)')
    stmt.run(title, body, function () {
      resolve({ id: this.lastID, title, body })
    })
    stmt.finalize()
  })

exports.getAllNotes = () =>
  new Promise(resolve => {
    db.all('SELECT * FROM notes', (_, rows) => resolve(rows))
  })

exports.getNote = id =>
  new Promise(resolve => {
    db.get('SELECT * FROM notes WHERE id=?', [id], (_, row) => resolve(row))
  })

exports.updateNote = (id, { title, body }) =>
  new Promise(resolve => {
    const stmt = db.prepare('UPDATE notes SET title=?, body=? WHERE id=?')
    stmt.run(title, body, id, function () {
      if (this.changes === 0) return resolve(null)
      resolve({ id, title, body })
    })
    stmt.finalize()
  })

exports.deleteNote = id =>
  new Promise(resolve => {
    const stmt = db.prepare('DELETE FROM notes WHERE id=?')
    stmt.run(id, function () {
      resolve(this.changes > 0)
    })
    stmt.finalize()
  })

Only the model changed; the routes remain identical. Rerun your tests—all should still pass because they only care about the public contract, not the implementation.

Step 10: Secure Your API

Production APIs must whitelist origins, throttle heavy users, trap errors, and escape inputs. Add these essentials:

Input Validation

npm install express-validator

Replace the POST route with:

const { body, validationResult } = require('express-validator')

router.post('/',
  body('title').notEmpty().withMessage('title required'),
  body('body').notEmpty().withMessage('body required'),
  async (req, res) => {
    const errors = validationResult(req)
    if (!errors.isEmpty())
      return res.status(400).json({ errors: errors.array() })
    const note = await Note.createNote(req.body)
    res.status(201).json(note)
  })

Error Handler Middleware

// middleware/errorHandler.js
module.exports = (err, req, res, next) => {
  console.error(err)
  res.status(err.status || 500).json({ message: 'Server Error' })
}

Register it as the last middleware in index.js:

app.use(require('./middleware/errorHandler'))

CORS and Rate Limiting

npm install cors express-rate-limit
const cors = require('cors')
const rateLimit = require('express-rate-limit')

app.use(cors())
app.use(rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }))

Restart the server—your server now blocks abusive traffic and allows browsers to call the API from any origin.

Step 11: Deploy for Free in Minutes

Create a Start Script

npm pkg set scripts.start="node src/index.js"

Prepare for Production

Ignore dev files and db in .gitignore:

node_modules/
.env
*.sqlite  
railway generated file
npx railway login
railway init
railway up

Railway detects a Node project and exposes the deployment URL. Open your Railway dashboard, add a PORT variable (the platform provides the real port automatically), and your API is live.

Step 12: Document Your API

Tools such as Swagger generate beautiful documentation, but a simple README.md works when starting out:

# Notes API

Base URL: https://your-deployed-api.railway.app

Endpoints:
- POST /notes  -d '{"title":"","body":""}'
- GET  /notes  - returns list
- GET  /notes/:id - single note
- PATCH /notes/:id - update
- DELETE /notes/:id

With tests, deployment, and documentation done, you have built and shipped a working REST API with only 60 lines of Express code.

Common Pitfalls to Avoid

  • Version your URLs: Start with /v1 so future breaking changes do not break existing users.
  • Use meaningful status codes: Incorrect codes confuse client SDKs and debug logs.
  • Return JSON only: Avoid HTML errors that cannot be parsed programmatically.
  • Log requests sparingly: Do not print full bodies; they may contain passwords.
  • Keep router files thin: Offload business logic to models or services so you can unit-test them.

Next Steps

From here you can:

  1. Swap SQLite for PostgreSQL or MongoDB.
  2. Replace In-Memory Authentication with JWT tokens.
  3. Add comprehensive OpenAPI documentation using swagger-jsdoc and swagger-ui-express.
  4. Introduce pagination on the GET /notes route for bigger datasets.
  5. Separate the project into a well-structured microservice, complete with Docker.

Wrap-Up

You just completed building a REST API from the ground up without skipping over testing or deployment. The pattern—validate data, model the domain, expose endpoints, secure the edges, and ship to the cloud—remains the same no matter the scale.

Note: This article was created by an AI. All code samples were tested by a human editor for correctness. Please adapt paths and versions to your own environment.
← Назад

Читайте также