Cara Membuat API dengan Node.js 2026: Tutorial REST API Lengkap
Node.js adalah platform terpopuler untuk membuat backend API. Di tutorial ini, kita akan buat REST API lengkap dengan authentication, validation, error handling, dan best practices. Let’s code!
Setup Project
mkdir my-api
cd my-api
npm init -y
# Install dependencies
npm install express mongoose dotenv bcryptjs jsonwebtoken
npm install -D nodemon
# Create structure
mkdir src
mkdir src/models src/routes src/controllers src/middleware
touch src/server.js .env .gitignore
Basic Express Server
// src/server.js
const express = require('express');
const mongoose = require('mongoose');
require('dotenv').config();
const app = express();
// Middleware
app.use(express.json());
// Routes
app.get('/', (req, res) => {
res.json({ message: 'API is running' });
});
// Database connection
mongoose.connect(process.env.MONGODB_URI)
.then(() => console.log('MongoDB connected'))
.catch(err => console.error(err));
// Start server
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
User Model
// src/models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const userSchema = new mongoose.Schema({
name: {
type: String,
required: [true, 'Name is required'],
trim: true
},
email: {
type: String,
required: [true, 'Email is required'],
unique: true,
lowercase: true,
match: [/^\S+@\S+\.\S+$/, 'Invalid email']
},
password: {
type: String,
required: [true, 'Password is required'],
minlength: 6,
select: false
},
role: {
type: String,
enum: ['user', 'admin'],
default: 'user'
}
}, {
timestamps: true
});
// Hash password before save
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
this.password = await bcrypt.hash(this.password, 10);
next();
});
// Compare password method
userSchema.methods.comparePassword = async function(password) {
return await bcrypt.compare(password, this.password);
};
module.exports = mongoose.model('User', userSchema);
Authentication Controller
// src/controllers/authController.js
const User = require('../models/User');
const jwt = require('jsonwebtoken');
// Generate JWT
const generateToken = (id) => {
return jwt.sign({ id }, process.env.JWT_SECRET, {
expiresIn: '30d'
});
};
// Register
exports.register = async (req, res) => {
try {
const { name, email, password } = req.body;
// Check if user exists
const userExists = await User.findOne({ email });
if (userExists) {
return res.status(400).json({ message: 'User already exists' });
}
// Create user
const user = await User.create({ name, email, password });
res.status(201).json({
_id: user._id,
name: user.name,
email: user.email,
token: generateToken(user._id)
});
} catch (error) {
res.status(500).json({ message: error.message });
}
};
// Login
exports.login = async (req, res) => {
try {
const { email, password } = req.body;
// Find user
const user = await User.findOne({ email }).select('+password');
if (!user) {
return res.status(401).json({ message: 'Invalid credentials' });
}
// Check password
const isMatch = await user.comparePassword(password);
if (!isMatch) {
return res.status(401).json({ message: 'Invalid credentials' });
}
res.json({
_id: user._id,
name: user.name,
email: user.email,
token: generateToken(user._id)
});
} catch (error) {
res.status(500).json({ message: error.message });
}
};
// Get current user
exports.getMe = async (req, res) => {
res.json(req.user);
};
Auth Middleware
// src/middleware/auth.js
const jwt = require('jsonwebtoken');
const User = require('../models/User');
exports.protect = async (req, res, next) => {
try {
let token;
// Get token from header
if (req.headers.authorization?.startsWith('Bearer')) {
token = req.headers.authorization.split(' ')[1];
}
if (!token) {
return res.status(401).json({ message: 'Not authorized' });
}
// Verify token
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// Get user
req.user = await User.findById(decoded.id);
if (!req.user) {
return res.status(401).json({ message: 'User not found' });
}
next();
} catch (error) {
res.status(401).json({ message: 'Not authorized' });
}
};
// Admin only
exports.admin = (req, res, next) => {
if (req.user.role !== 'admin') {
return res.status(403).json({ message: 'Admin access required' });
}
next();
};
Routes
// src/routes/authRoutes.js
const express = require('express');
const router = express.Router();
const { register, login, getMe } = require('../controllers/authController');
const { protect } = require('../middleware/auth');
router.post('/register', register);
router.post('/login', login);
router.get('/me', protect, getMe);
module.exports = router;
CRUD Operations
// src/controllers/productController.js
const Product = require('../models/Product');
// Get all products
exports.getProducts = async (req, res) => {
try {
const products = await Product.find();
res.json(products);
} catch (error) {
res.status(500).json({ message: error.message });
}
};
// Get single product
exports.getProduct = async (req, res) => {
try {
const product = await Product.findById(req.params.id);
if (!product) {
return res.status(404).json({ message: 'Product not found' });
}
res.json(product);
} catch (error) {
res.status(500).json({ message: error.message });
}
};
// Create product
exports.createProduct = async (req, res) => {
try {
const product = await Product.create(req.body);
res.status(201).json(product);
} catch (error) {
res.status(400).json({ message: error.message });
}
};
// Update product
exports.updateProduct = async (req, res) => {
try {
const product = await Product.findByIdAndUpdate(
req.params.id,
req.body,
{ new: true, runValidators: true }
);
if (!product) {
return res.status(404).json({ message: 'Product not found' });
}
res.json(product);
} catch (error) {
res.status(400).json({ message: error.message });
}
};
// Delete product
exports.deleteProduct = async (req, res) => {
try {
const product = await Product.findByIdAndDelete(req.params.id);
if (!product) {
return res.status(404).json({ message: 'Product not found' });
}
res.json({ message: 'Product deleted' });
} catch (error) {
res.status(500).json({ message: error.message });
}
};
Validation Middleware
// src/middleware/validate.js
const { validationResult } = require('express-validator');
exports.validate = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
next();
};
Error Handler
// src/middleware/errorHandler.js
exports.errorHandler = (err, req, res, next) => {
console.error(err.stack);
res.status(err.statusCode || 500).json({
message: err.message || 'Server Error',
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
});
};
Pagination & Filtering
exports.getProducts = async (req, res) => {
try {
// Pagination
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const skip = (page - 1) * limit;
// Filtering
const filter = {};
if (req.query.category) filter.category = req.query.category;
if (req.query.minPrice) filter.price = { $gte: req.query.minPrice };
// Sorting
const sort = req.query.sort || '-createdAt';
// Execute query
const products = await Product.find(filter)
.sort(sort)
.skip(skip)
.limit(limit);
const total = await Product.countDocuments(filter);
res.json({
products,
page,
pages: Math.ceil(total / limit),
total
});
} catch (error) {
res.status(500).json({ message: error.message });
}
};
File Upload
const multer = require('multer');
const path = require('path');
// Storage config
const storage = multer.diskStorage({
destination: './uploads/',
filename: (req, file, cb) => {
cb(null, `${Date.now()}-${file.originalname}`);
}
});
// File filter
const fileFilter = (req, file, cb) => {
const allowedTypes = /jpeg|jpg|png|gif/;
const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase());
const mimetype = allowedTypes.test(file.mimetype);
if (extname && mimetype) {
cb(null, true);
} else {
cb(new Error('Images only!'));
}
};
const upload = multer({
storage,
limits: { fileSize: 5 * 1024 * 1024 }, // 5MB
fileFilter
});
// Route
router.post('/upload', upload.single('image'), (req, res) => {
res.json({ file: req.file.filename });
});
Testing with Jest
// tests/auth.test.js
const request = require('supertest');
const app = require('../src/server');
describe('Auth API', () => {
it('should register a new user', async () => {
const res = await request(app)
.post('/api/auth/register')
.send({
name: 'Test User',
email: 'test@example.com',
password: 'password123'
});
expect(res.statusCode).toBe(201);
expect(res.body).toHaveProperty('token');
});
it('should login user', async () => {
const res = await request(app)
.post('/api/auth/login')
.send({
email: 'test@example.com',
password: 'password123'
});
expect(res.statusCode).toBe(200);
expect(res.body).toHaveProperty('token');
});
});
Deployment
Environment Variables
NODE_ENV=production
PORT=5000
MONGODB_URI=mongodb+srv://...
JWT_SECRET=your_secret_key
PM2 (Process Manager)
npm install -g pm2
pm2 start src/server.js --name my-api
pm2 startup
pm2 save
Docker
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --production
COPY . .
EXPOSE 5000
CMD ["node", "src/server.js"]
Best Practices
✅ Use environment variables ✅ Validate input data ✅ Handle errors properly ✅ Use async/await ✅ Implement rate limiting ✅ Add CORS ✅ Use HTTPS ✅ Log requests ✅ Monitor performance ✅ Write tests
Kesimpulan
Membuat API dengan Node.js di 2026 sangat straightforward dengan tools modern. Key points:
- Express untuk routing
- MongoDB untuk database
- JWT untuk authentication
- Middleware untuk reusable logic
- Best practices untuk production-ready
Artikel Terkait
- Belajar React JS untuk Pemula 2026 - Frontend untuk API Anda
- Panduan Docker untuk Developer 2026 - Deploy API dengan Docker
- Optimasi Database MySQL 2026 - Database optimization
Butuh bantuan develop API profesional? Hydra Core Digitech siap bantu. Konsultasi gratis!
