- Published on
Tutorial: Membangun API RESTful dengan Node.js dan Express
- Authors
- Name
- Anonymous Developer
- @bekasidev
Tutorial: Membangun API RESTful dengan Node.js dan Express
Dalam tutorial ini, kita akan membangun API RESTful yang lengkap menggunakan Node.js dan Express. Tutorial ini dirancang khusus untuk developer di Bekasi yang ingin menguasai backend development.
๐ฏ Apa yang Akan Kita Bangun?
Kita akan membuat Task Management API dengan fitur:
- User authentication & authorization
- CRUD operations untuk tasks
- File upload & management
- Input validation & error handling
- API documentation dengan Swagger
๐ Prerequisites
Yang Perlu Dipahami:
- JavaScript ES6+ basics
- Async/await dan Promises
- Basic HTTP concepts (GET, POST, PUT, DELETE)
- Command line usage
Tools yang Dibutuhkan:
- Node.js (v18 atau lebih baru)
- MongoDB (local atau Atlas)
- Postman atau Thunder Client
- Code editor (VS Code)
๐ Setup Project
1. Inisialisasi Project
mkdir bekasi-task-api
cd bekasi-task-api
npm init -y
2. Install Dependencies
# Core dependencies
npm install express mongoose dotenv cors helmet
# Development dependencies
npm install -D nodemon
# Authentication & validation
npm install bcryptjs jsonwebtoken joi
# File upload & utilities
npm install multer cloudinary
3. Project Structure
bekasi-task-api/
โโโ src/
โ โโโ controllers/
โ โโโ models/
โ โโโ routes/
โ โโโ middleware/
โ โโโ utils/
โ โโโ config/
โโโ uploads/
โโโ .env
โโโ .gitignore
โโโ server.js
4. Package.json Scripts
{
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"test": "echo \"Error: no test specified\" && exit 1"
}
}
๐ง Konfigurasi Dasar
Environment Variables (.env)
PORT=3000
MONGODB_URI=mongodb://localhost:27017/bekasi-task-api
JWT_SECRET=bekasi_dev_community_secret_key_2024
JWT_EXPIRE=7d
NODE_ENV=development
# Cloudinary (optional, untuk file upload)
CLOUDINARY_CLOUD_NAME=your_cloud_name
CLOUDINARY_API_KEY=your_api_key
CLOUDINARY_API_SECRET=your_api_secret
Database Connection (src/config/database.js)
const mongoose = require('mongoose');
const connectDB = async () => {
try {
const conn = await mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
console.log(`MongoDB Connected: ${conn.connection.host}`);
} catch (error) {
console.error('Database connection error:', error);
process.exit(1);
}
};
module.exports = connectDB;
Main Server File (server.js)
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const dotenv = require('dotenv');
// Load env vars
dotenv.config();
// Import configs
const connectDB = require('./src/config/database');
// Connect to database
connectDB();
const app = express();
// Security middleware
app.use(helmet());
app.use(cors());
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
// Routes
app.use('/api/auth', require('./src/routes/auth'));
app.use('/api/tasks', require('./src/routes/tasks'));
// Error handling middleware
app.use(require('./src/middleware/errorHandler'));
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
๐ Models (Database Schema)
User Model (src/models/User.js)
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const UserSchema = new mongoose.Schema({
name: {
type: String,
required: [true, 'Please add a name'],
trim: true,
maxlength: [50, 'Name cannot be more than 50 characters']
},
email: {
type: String,
required: [true, 'Please add an email'],
unique: true,
lowercase: true,
match: [
/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/,
'Please add a valid email'
]
},
password: {
type: String,
required: [true, 'Please add a password'],
minlength: [6, 'Password must be at least 6 characters'],
select: false
},
role: {
type: String,
enum: ['user', 'admin'],
default: 'user'
},
avatar: {
type: String,
default: null
},
createdAt: {
type: Date,
default: Date.now
}
});
// Encrypt password using bcrypt
UserSchema.pre('save', async function(next) {
if (!this.isModified('password')) {
next();
}
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
});
// Match user entered password to hashed password in database
UserSchema.methods.matchPassword = async function(enteredPassword) {
return await bcrypt.compare(enteredPassword, this.password);
};
module.exports = mongoose.model('User', UserSchema);
Task Model (src/models/Task.js)
const mongoose = require('mongoose');
const TaskSchema = new mongoose.Schema({
title: {
type: String,
required: [true, 'Please add a task title'],
trim: true,
maxlength: [100, 'Title cannot be more than 100 characters']
},
description: {
type: String,
maxlength: [500, 'Description cannot be more than 500 characters']
},
status: {
type: String,
enum: ['pending', 'in-progress', 'completed'],
default: 'pending'
},
priority: {
type: String,
enum: ['low', 'medium', 'high'],
default: 'medium'
},
dueDate: {
type: Date
},
tags: [{
type: String,
trim: true
}],
attachments: [{
filename: String,
url: String,
fileType: String,
uploadedAt: {
type: Date,
default: Date.now
}
}],
user: {
type: mongoose.Schema.ObjectId,
ref: 'User',
required: true
},
createdAt: {
type: Date,
default: Date.now
},
updatedAt: {
type: Date,
default: Date.now
}
});
// Update updatedAt field before saving
TaskSchema.pre('save', function(next) {
this.updatedAt = Date.now();
next();
});
module.exports = mongoose.model('Task', TaskSchema);
๐ Authentication & Middleware
JWT Utility (src/utils/jwt.js)
const jwt = require('jsonwebtoken');
// Generate JWT token
const generateToken = (id) => {
return jwt.sign({ id }, process.env.JWT_SECRET, {
expiresIn: process.env.JWT_EXPIRE,
});
};
// Verify JWT token
const verifyToken = (token) => {
return jwt.verify(token, process.env.JWT_SECRET);
};
module.exports = { generateToken, verifyToken };
Auth Middleware (src/middleware/auth.js)
const User = require('../models/User');
const { verifyToken } = require('../utils/jwt');
// Protect routes
const protect = async (req, res, next) => {
let token;
if (req.headers.authorization && req.headers.authorization.startsWith('Bearer')) {
token = req.headers.authorization.split(' ')[1];
}
if (!token) {
return res.status(401).json({
success: false,
message: 'Not authorized to access this route'
});
}
try {
const decoded = verifyToken(token);
req.user = await User.findById(decoded.id);
next();
} catch (error) {
return res.status(401).json({
success: false,
message: 'Not authorized to access this route'
});
}
};
// Grant access to specific roles
const authorize = (...roles) => {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({
success: false,
message: 'User role is not authorized to access this route'
});
}
next();
};
};
module.exports = { protect, authorize };
Validation Middleware (src/middleware/validation.js)
const Joi = require('joi');
// Validate request data
const validate = (schema) => {
return (req, res, next) => {
const { error } = schema.validate(req.body);
if (error) {
const errors = error.details.map(detail => detail.message);
return res.status(400).json({
success: false,
message: 'Validation error',
errors
});
}
next();
};
};
// Validation schemas
const schemas = {
register: Joi.object({
name: Joi.string().min(2).max(50).required(),
email: Joi.string().email().required(),
password: Joi.string().min(6).required()
}),
login: Joi.object({
email: Joi.string().email().required(),
password: Joi.string().required()
}),
createTask: Joi.object({
title: Joi.string().min(1).max(100).required(),
description: Joi.string().max(500),
status: Joi.string().valid('pending', 'in-progress', 'completed'),
priority: Joi.string().valid('low', 'medium', 'high'),
dueDate: Joi.date(),
tags: Joi.array().items(Joi.string())
}),
updateTask: Joi.object({
title: Joi.string().min(1).max(100),
description: Joi.string().max(500),
status: Joi.string().valid('pending', 'in-progress', 'completed'),
priority: Joi.string().valid('low', 'medium', 'high'),
dueDate: Joi.date(),
tags: Joi.array().items(Joi.string())
})
};
module.exports = { validate, schemas };
๐ฎ Controllers
Auth Controller (src/controllers/authController.js)
const User = require('../models/User');
const { generateToken } = require('../utils/jwt');
// @desc Register user
// @route POST /api/auth/register
// @access Public
const register = async (req, res) => {
try {
const { name, email, password } = req.body;
// Check if user exists
const existingUser = await User.findOne({ email });
if (existingUser) {
return res.status(400).json({
success: false,
message: 'User already exists'
});
}
// Create user
const user = await User.create({
name,
email,
password
});
// Generate token
const token = generateToken(user._id);
res.status(201).json({
success: true,
message: 'User registered successfully',
data: {
user: {
id: user._id,
name: user.name,
email: user.email,
role: user.role
},
token
}
});
} catch (error) {
res.status(500).json({
success: false,
message: 'Server error',
error: error.message
});
}
};
// @desc Login user
// @route POST /api/auth/login
// @access Public
const login = async (req, res) => {
try {
const { email, password } = req.body;
// Check for user
const user = await User.findOne({ email }).select('+password');
if (!user) {
return res.status(401).json({
success: false,
message: 'Invalid credentials'
});
}
// Check password
const isMatch = await user.matchPassword(password);
if (!isMatch) {
return res.status(401).json({
success: false,
message: 'Invalid credentials'
});
}
// Generate token
const token = generateToken(user._id);
res.status(200).json({
success: true,
message: 'Login successful',
data: {
user: {
id: user._id,
name: user.name,
email: user.email,
role: user.role
},
token
}
});
} catch (error) {
res.status(500).json({
success: false,
message: 'Server error',
error: error.message
});
}
};
// @desc Get current user
// @route GET /api/auth/me
// @access Private
const getMe = async (req, res) => {
try {
const user = await User.findById(req.user.id);
res.status(200).json({
success: true,
data: {
user
}
});
} catch (error) {
res.status(500).json({
success: false,
message: 'Server error',
error: error.message
});
}
};
module.exports = {
register,
login,
getMe
};
Task Controller (src/controllers/taskController.js)
const Task = require('../models/Task');
// @desc Get all tasks
// @route GET /api/tasks
// @access Private
const getTasks = async (req, res) => {
try {
const { status, priority, page = 1, limit = 10 } = req.query;
// Build query
const query = { user: req.user.id };
if (status) query.status = status;
if (priority) query.priority = priority;
// Execute query with pagination
const tasks = await Task.find(query)
.sort({ createdAt: -1 })
.limit(limit * 1)
.skip((page - 1) * limit);
const total = await Task.countDocuments(query);
res.status(200).json({
success: true,
count: tasks.length,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
pages: Math.ceil(total / limit)
},
data: tasks
});
} catch (error) {
res.status(500).json({
success: false,
message: 'Server error',
error: error.message
});
}
};
// @desc Get single task
// @route GET /api/tasks/:id
// @access Private
const getTask = async (req, res) => {
try {
const task = await Task.findOne({
_id: req.params.id,
user: req.user.id
});
if (!task) {
return res.status(404).json({
success: false,
message: 'Task not found'
});
}
res.status(200).json({
success: true,
data: task
});
} catch (error) {
res.status(500).json({
success: false,
message: 'Server error',
error: error.message
});
}
};
// @desc Create new task
// @route POST /api/tasks
// @access Private
const createTask = async (req, res) => {
try {
// Add user to req.body
req.body.user = req.user.id;
const task = await Task.create(req.body);
res.status(201).json({
success: true,
message: 'Task created successfully',
data: task
});
} catch (error) {
res.status(500).json({
success: false,
message: 'Server error',
error: error.message
});
}
};
// @desc Update task
// @route PUT /api/tasks/:id
// @access Private
const updateTask = async (req, res) => {
try {
let task = await Task.findOne({
_id: req.params.id,
user: req.user.id
});
if (!task) {
return res.status(404).json({
success: false,
message: 'Task not found'
});
}
task = await Task.findByIdAndUpdate(req.params.id, req.body, {
new: true,
runValidators: true
});
res.status(200).json({
success: true,
message: 'Task updated successfully',
data: task
});
} catch (error) {
res.status(500).json({
success: false,
message: 'Server error',
error: error.message
});
}
};
// @desc Delete task
// @route DELETE /api/tasks/:id
// @access Private
const deleteTask = async (req, res) => {
try {
const task = await Task.findOne({
_id: req.params.id,
user: req.user.id
});
if (!task) {
return res.status(404).json({
success: false,
message: 'Task not found'
});
}
await Task.findByIdAndDelete(req.params.id);
res.status(200).json({
success: true,
message: 'Task deleted successfully'
});
} catch (error) {
res.status(500).json({
success: false,
message: 'Server error',
error: error.message
});
}
};
module.exports = {
getTasks,
getTask,
createTask,
updateTask,
deleteTask
};
๐ฃ๏ธ Routes
Auth Routes (src/routes/auth.js)
const express = require('express');
const { register, login, getMe } = require('../controllers/authController');
const { protect } = require('../middleware/auth');
const { validate, schemas } = require('../middleware/validation');
const router = express.Router();
router.post('/register', validate(schemas.register), register);
router.post('/login', validate(schemas.login), login);
router.get('/me', protect, getMe);
module.exports = router;
Task Routes (src/routes/tasks.js)
const express = require('express');
const {
getTasks,
getTask,
createTask,
updateTask,
deleteTask
} = require('../controllers/taskController');
const { protect } = require('../middleware/auth');
const { validate, schemas } = require('../middleware/validation');
const router = express.Router();
// Protect all routes
router.use(protect);
router
.route('/')
.get(getTasks)
.post(validate(schemas.createTask), createTask);
router
.route('/:id')
.get(getTask)
.put(validate(schemas.updateTask), updateTask)
.delete(deleteTask);
module.exports = router;
๐งช Testing API
Test dengan Postman/Thunder Client
1. Register User
POST http://localhost:3000/api/auth/register
Content-Type: application/json
{
"name": "Developer Bekasi",
"email": "developer@bekasi.dev",
"password": "password123"
}
2. Login
POST http://localhost:3000/api/auth/login
Content-Type: application/json
{
"email": "developer@bekasi.dev",
"password": "password123"
}
3. Create Task
POST http://localhost:3000/api/tasks
Authorization: Bearer YOUR_JWT_TOKEN
Content-Type: application/json
{
"title": "Belajar Node.js API",
"description": "Tutorial lengkap membuat RESTful API",
"priority": "high",
"dueDate": "2024-04-01",
"tags": ["learning", "nodejs", "api"]
}
4. Get Tasks
GET http://localhost:3000/api/tasks?status=pending&page=1&limit=5
Authorization: Bearer YOUR_JWT_TOKEN
๐ Advanced Features
File Upload dengan Multer
const multer = require('multer');
const path = require('path');
// Configure multer
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, 'uploads/');
},
filename: function (req, file, cb) {
cb(null, Date.now() + '-' + Math.round(Math.random() * 1E9) + path.extname(file.originalname));
}
});
const upload = multer({
storage: storage,
limits: {
fileSize: 5 * 1024 * 1024 // 5MB
},
fileFilter: function (req, file, cb) {
const allowedTypes = /jpeg|jpg|png|pdf|doc|docx/;
const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase());
const mimetype = allowedTypes.test(file.mimetype);
if (mimetype && extname) {
return cb(null, true);
} else {
cb(new Error('Only images and documents are allowed'));
}
}
});
module.exports = upload;
Error Handling Middleware
// src/middleware/errorHandler.js
const errorHandler = (err, req, res, next) => {
let error = { ...err };
error.message = err.message;
// Log to console for dev
console.log(err);
// Mongoose bad ObjectId
if (err.name === 'CastError') {
const message = 'Resource not found';
error = { message, statusCode: 404 };
}
// Mongoose duplicate key
if (err.code === 11000) {
const message = 'Duplicate field value entered';
error = { message, statusCode: 400 };
}
// Mongoose validation error
if (err.name === 'ValidationError') {
const message = Object.values(err.errors).map(val => val.message);
error = { message, statusCode: 400 };
}
res.status(error.statusCode || 500).json({
success: false,
message: error.message || 'Server Error'
});
};
module.exports = errorHandler;
๐ API Documentation
Generate dengan Swagger
const swaggerJsdoc = require('swagger-jsdoc');
const swaggerUi = require('swagger-ui-express');
const options = {
definition: {
openapi: '3.0.0',
info: {
title: 'Bekasi Task API',
version: '1.0.0',
description: 'Task Management API for Bekasi Developers',
},
servers: [
{
url: 'http://localhost:3000',
description: 'Development server',
},
],
},
apis: ['./src/routes/*.js'], // Path to the API docs
};
const specs = swaggerJsdoc(options);
// Add to server.js
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs));
๐ Deployment
Deployment ke Railway
# Install Railway CLI
npm install -g @railway/cli
# Login
railway login
# Initialize project
railway init
# Deploy
railway up
Environment Variables untuk Production
NODE_ENV=production
MONGODB_URI=mongodb+srv://username:password@cluster.mongodb.net/bekasi-task-api
JWT_SECRET=super_secure_secret_key_for_production
PORT=3000
Selamat! Anda telah berhasil membuat API RESTful yang lengkap dengan Node.js dan Express. API ini siap digunakan untuk aplikasi frontend atau mobile app.
Tutorial selanjutnya: "Mengintegrasikan API dengan React Frontend" - coming soon!
#NodeJS #ExpressJS #API #BackendDevelopment #BekasiDev